psdi-data-conversion 0.1.7__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- psdi_data_conversion/app.py +5 -408
- psdi_data_conversion/constants.py +12 -8
- psdi_data_conversion/converter.py +41 -28
- psdi_data_conversion/converters/base.py +18 -13
- psdi_data_conversion/database.py +292 -88
- psdi_data_conversion/gui/__init__.py +5 -0
- psdi_data_conversion/gui/accessibility.py +51 -0
- psdi_data_conversion/gui/env.py +239 -0
- psdi_data_conversion/gui/get.py +53 -0
- psdi_data_conversion/gui/post.py +176 -0
- psdi_data_conversion/gui/setup.py +102 -0
- psdi_data_conversion/main.py +70 -13
- psdi_data_conversion/static/content/convert.htm +105 -74
- psdi_data_conversion/static/content/convertato.htm +36 -26
- psdi_data_conversion/static/content/convertc2x.htm +39 -26
- psdi_data_conversion/static/content/download.htm +5 -5
- psdi_data_conversion/static/content/feedback.htm +2 -2
- psdi_data_conversion/static/content/header-links.html +2 -2
- psdi_data_conversion/static/content/index-versions/header-links.html +2 -2
- psdi_data_conversion/static/content/index-versions/psdi-common-header.html +9 -12
- psdi_data_conversion/static/content/psdi-common-header.html +9 -12
- psdi_data_conversion/static/javascript/accessibility.js +88 -61
- psdi_data_conversion/static/javascript/data.js +1 -3
- psdi_data_conversion/static/javascript/load_accessibility.js +50 -33
- psdi_data_conversion/static/styles/format.css +72 -18
- psdi_data_conversion/templates/accessibility.htm +274 -0
- psdi_data_conversion/templates/documentation.htm +6 -6
- psdi_data_conversion/templates/index.htm +73 -56
- psdi_data_conversion/{static/content → templates}/report.htm +28 -10
- psdi_data_conversion/testing/conversion_test_specs.py +26 -6
- psdi_data_conversion/testing/utils.py +6 -6
- {psdi_data_conversion-0.1.7.dist-info → psdi_data_conversion-0.2.1.dist-info}/METADATA +9 -3
- {psdi_data_conversion-0.1.7.dist-info → psdi_data_conversion-0.2.1.dist-info}/RECORD +36 -30
- psdi_data_conversion/static/content/accessibility.htm +0 -255
- {psdi_data_conversion-0.1.7.dist-info → psdi_data_conversion-0.2.1.dist-info}/WHEEL +0 -0
- {psdi_data_conversion-0.1.7.dist-info → psdi_data_conversion-0.2.1.dist-info}/entry_points.txt +0 -0
- {psdi_data_conversion-0.1.7.dist-info → psdi_data_conversion-0.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
"""
|
2
|
+
# post.py
|
3
|
+
|
4
|
+
This module defines the various web addresses which do something (the "POST" methods) provided by the website,
|
5
|
+
connecting them to relevant functions.
|
6
|
+
"""
|
7
|
+
|
8
|
+
|
9
|
+
import json
|
10
|
+
|
11
|
+
from flask import Flask, make_response, render_template, request
|
12
|
+
|
13
|
+
from psdi_data_conversion import constants as const
|
14
|
+
from psdi_data_conversion.gui.env import get_env_kwargs
|
15
|
+
|
16
|
+
|
17
|
+
def accessibility():
|
18
|
+
"""Return the accessibility page
|
19
|
+
"""
|
20
|
+
return render_template("accessibility.htm",
|
21
|
+
**get_env_kwargs())
|
22
|
+
|
23
|
+
|
24
|
+
def save_accessibility():
|
25
|
+
"""Save the user's accessibility settings in a cookie
|
26
|
+
"""
|
27
|
+
|
28
|
+
resp = make_response("Cookie saved successfully")
|
29
|
+
|
30
|
+
d_settings: dict[str, str] = json.loads(request.form['data'])
|
31
|
+
|
32
|
+
for key, val in d_settings.items():
|
33
|
+
resp.set_cookie(key, val, max_age=const.YEAR)
|
34
|
+
|
35
|
+
return resp
|
36
|
+
|
37
|
+
|
38
|
+
def load_accessibility():
|
39
|
+
"""Load the user's accessibility settings from the cookie
|
40
|
+
"""
|
41
|
+
return json.dumps(request.cookies)
|
42
|
+
|
43
|
+
|
44
|
+
def init_accessibility(app: Flask):
|
45
|
+
"""Connect the provided Flask app to each of the post methods
|
46
|
+
"""
|
47
|
+
|
48
|
+
app.route('/accessibility.htm')(accessibility)
|
49
|
+
|
50
|
+
app.route('/save_accessibility/', methods=["POST"])(save_accessibility)
|
51
|
+
app.route('/load_accessibility/', methods=["GET"])(load_accessibility)
|
@@ -0,0 +1,239 @@
|
|
1
|
+
"""
|
2
|
+
# env.py
|
3
|
+
|
4
|
+
This module handles setting up and storing the state of the environment for the website, e.g. environmental variables
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import sys
|
9
|
+
from argparse import Namespace
|
10
|
+
from datetime import datetime
|
11
|
+
from hashlib import md5
|
12
|
+
from subprocess import run
|
13
|
+
from traceback import format_exc
|
14
|
+
from typing import TypeVar
|
15
|
+
|
16
|
+
from psdi_data_conversion import constants as const
|
17
|
+
from psdi_data_conversion import log_utility
|
18
|
+
|
19
|
+
# Env var for the tag and SHA of the latest commit
|
20
|
+
TAG_EV = "TAG"
|
21
|
+
TAG_SHA_EV = "TAG_SHA"
|
22
|
+
SHA_EV = "SHA"
|
23
|
+
|
24
|
+
# Env var for whether this is a production release or development
|
25
|
+
PRODUCTION_EV = "PRODUCTION_MODE"
|
26
|
+
|
27
|
+
# Env var for whether this is a production release or development
|
28
|
+
DEBUG_EV = "DEBUG_MODE"
|
29
|
+
|
30
|
+
|
31
|
+
class SiteEnv:
|
32
|
+
def __init__(self, args: Namespace | None = None):
|
33
|
+
|
34
|
+
self._args = args
|
35
|
+
"""The parsed arguments provided to the script to start the server"""
|
36
|
+
|
37
|
+
self.log_mode: str = self._determine_log_mode()
|
38
|
+
"""The logging mode"""
|
39
|
+
|
40
|
+
self.log_level: str = self._determine_log_level()
|
41
|
+
"""The logging level"""
|
42
|
+
|
43
|
+
self.max_file_size = self._determine_value(ev=const.MAX_FILESIZE_EV,
|
44
|
+
arg="max_file_size",
|
45
|
+
default=const.DEFAULT_MAX_FILE_SIZE /
|
46
|
+
const.MEGABYTE)*const.MEGABYTE
|
47
|
+
"""The maximum file size for converters other than Open Babel"""
|
48
|
+
|
49
|
+
self.max_file_size_ob = self._determine_value(ev=const.MAX_FILESIZE_OB_EV,
|
50
|
+
arg="max_file_size_ob",
|
51
|
+
default=const.DEFAULT_MAX_FILE_SIZE_OB /
|
52
|
+
const.MEGABYTE)*const.MEGABYTE
|
53
|
+
"""The maximum file size for the Open Babel converter"""
|
54
|
+
|
55
|
+
self.service_mode = self._determine_value(ev=const.SERVICE_MODE_EV,
|
56
|
+
arg="service_mode",
|
57
|
+
value_type=bool,
|
58
|
+
default=False)
|
59
|
+
"""True if the app is running in service mode, False if it's running in local mode"""
|
60
|
+
|
61
|
+
self.production_mode = self._determine_value(ev=PRODUCTION_EV,
|
62
|
+
arg="!dev_mode",
|
63
|
+
value_type=bool,
|
64
|
+
default=False)
|
65
|
+
"""True if the app is running in production mode, False if it's running in developmennt mode"""
|
66
|
+
|
67
|
+
self.debug_mode = self._determine_value(ev=DEBUG_EV,
|
68
|
+
arg="debug",
|
69
|
+
value_type=bool,
|
70
|
+
default=False)
|
71
|
+
"""True if the app is running in debug mode, False if not"""
|
72
|
+
|
73
|
+
tag, sha = self._determine_tag_and_sha()
|
74
|
+
|
75
|
+
self.tag: str = tag
|
76
|
+
"""The latest tag in the repo"""
|
77
|
+
|
78
|
+
self.sha: str = sha
|
79
|
+
"""The SHA of the latest commit, if the latest commit isn't tagged, otherwise an empty string"""
|
80
|
+
|
81
|
+
dt = str(datetime.now())
|
82
|
+
self.token = md5(dt.encode('utf8')).hexdigest()
|
83
|
+
"""A token for this session, created by hashing the the current date and time"""
|
84
|
+
|
85
|
+
self._kwargs: dict[str, str] | None = None
|
86
|
+
"""Cached value for dict containing all env values"""
|
87
|
+
|
88
|
+
@property
|
89
|
+
def kwargs(self) -> dict[str, str]:
|
90
|
+
"""Get a dict which can be used to provide kwargs for rendering a template"""
|
91
|
+
if not self._kwargs:
|
92
|
+
self._kwargs = {}
|
93
|
+
for key, val in self.__dict__.items():
|
94
|
+
if not key.startswith("_"):
|
95
|
+
self._kwargs[key] = val
|
96
|
+
return self._kwargs
|
97
|
+
|
98
|
+
def _determine_log_mode(self) -> str:
|
99
|
+
"""Determine the log mode from args and environmental variables, preferring the former"""
|
100
|
+
if self._args:
|
101
|
+
return self._args.log_mode
|
102
|
+
|
103
|
+
ev_log_mode = os.environ.get(const.LOG_MODE_EV)
|
104
|
+
if ev_log_mode is None:
|
105
|
+
return const.LOG_MODE_DEFAULT
|
106
|
+
|
107
|
+
ev_log_mode = ev_log_mode.lower()
|
108
|
+
if ev_log_mode not in const.L_ALLOWED_LOG_MODES:
|
109
|
+
raise ValueError(f"ERROR: Unrecognised logging option: {ev_log_mode}. "
|
110
|
+
f"Allowed options are: {const.L_ALLOWED_LOG_MODES}")
|
111
|
+
|
112
|
+
return ev_log_mode
|
113
|
+
|
114
|
+
def _determine_log_level(self) -> str | None:
|
115
|
+
"""Determine the log level from args and environmental variables, preferring the former"""
|
116
|
+
if self._args:
|
117
|
+
return self._args.log_level
|
118
|
+
|
119
|
+
ev_log_level = os.environ.get(const.LOG_LEVEL_EV)
|
120
|
+
if ev_log_level is None:
|
121
|
+
return None
|
122
|
+
|
123
|
+
return log_utility.get_log_level_from_str(ev_log_level)
|
124
|
+
|
125
|
+
T = TypeVar('T')
|
126
|
+
|
127
|
+
def _determine_value(self,
|
128
|
+
ev: str,
|
129
|
+
arg: str,
|
130
|
+
value_type: type[T] = float,
|
131
|
+
default: T = None) -> T | None:
|
132
|
+
"""Determine a value using input arguments (preferred if present) and environmental variables"""
|
133
|
+
if self._args and arg:
|
134
|
+
# Special handling for bool, which allows flipping the value of an arg
|
135
|
+
if value_type is bool and arg.startswith("!"):
|
136
|
+
return not getattr(self._args, arg[1:])
|
137
|
+
return getattr(self._args, arg)
|
138
|
+
|
139
|
+
ev_value = os.environ.get(ev)
|
140
|
+
if not ev_value:
|
141
|
+
return default
|
142
|
+
|
143
|
+
# Special handling for bool, to properly parse strings into bools
|
144
|
+
if value_type is bool:
|
145
|
+
return ev_value.lower().startswith("t")
|
146
|
+
|
147
|
+
return value_type(ev_value)
|
148
|
+
|
149
|
+
def _determine_tag_and_sha(self) -> tuple[str, str]:
|
150
|
+
"""Get latest tag and SHA of latest commit, if the latest commit differs from the latest tagged commit
|
151
|
+
"""
|
152
|
+
|
153
|
+
# Get the tag of the latest commit
|
154
|
+
ev_tag = os.environ.get(TAG_EV)
|
155
|
+
if ev_tag:
|
156
|
+
tag = ev_tag
|
157
|
+
else:
|
158
|
+
try:
|
159
|
+
# This bash command calls `git tag` to get a sorted list of tags, with the most recent at the top, then
|
160
|
+
# uses `head` to trim it to one line
|
161
|
+
cmd = "git tag --sort -version:refname | head -n 1"
|
162
|
+
|
163
|
+
out_bytes = run(cmd, shell=True, capture_output=True).stdout
|
164
|
+
tag = str(out_bytes.decode()).strip()
|
165
|
+
|
166
|
+
except Exception:
|
167
|
+
# Failsafe exception block, since this is reasonably likely to occur (e.g. due to a shallow fetch of the
|
168
|
+
# repo, and we don't want to crash the whole app because of it)
|
169
|
+
print("ERROR: Could not determine most recent tag. Error was:\n" + format_exc(),
|
170
|
+
file=sys.stderr)
|
171
|
+
tag = ""
|
172
|
+
|
173
|
+
# Get the SHA associated with this tag
|
174
|
+
ev_tag_sha = os.environ.get(TAG_SHA_EV)
|
175
|
+
if ev_tag_sha:
|
176
|
+
tag_sha: str | None = ev_tag_sha
|
177
|
+
else:
|
178
|
+
try:
|
179
|
+
cmd = f"git show {tag}" + " | head -n 1 | gawk '{print($2)}'"
|
180
|
+
|
181
|
+
out_bytes = run(cmd, shell=True, capture_output=True).stdout
|
182
|
+
tag_sha = str(out_bytes.decode()).strip()
|
183
|
+
|
184
|
+
except Exception:
|
185
|
+
# Another failsafe block, same reason as before
|
186
|
+
print("ERROR: Could not determine SHA for most recent tag. Error was:\n" + format_exc(),
|
187
|
+
file=sys.stderr)
|
188
|
+
tag_sha = None
|
189
|
+
|
190
|
+
# First check if the SHA is provided through an environmental variable
|
191
|
+
ev_sha = os.environ.get(SHA_EV)
|
192
|
+
if ev_sha:
|
193
|
+
sha = ev_sha
|
194
|
+
else:
|
195
|
+
try:
|
196
|
+
# This bash command calls `git log` to get info on the last commit, uses `head` to trim it to one line,
|
197
|
+
# then uses `gawk` to get just the second word of this line, which is the SHA of this commit
|
198
|
+
cmd = "git log -n 1 | head -n 1 | gawk '{print($2)}'"
|
199
|
+
|
200
|
+
out_bytes = run(cmd, shell=True, capture_output=True).stdout
|
201
|
+
sha = str(out_bytes.decode()).strip()
|
202
|
+
|
203
|
+
except Exception:
|
204
|
+
# Another failsafe block, same reason as before
|
205
|
+
print("ERROR: Could not determine SHA of most recent commit. Error was:\n" + format_exc(),
|
206
|
+
file=sys.stderr)
|
207
|
+
sha = ""
|
208
|
+
|
209
|
+
# If the SHA of the tag is the same as the current SHA, we indicate this by returning a blank SHA
|
210
|
+
if tag_sha == sha:
|
211
|
+
sha = ""
|
212
|
+
|
213
|
+
return (tag, sha)
|
214
|
+
|
215
|
+
|
216
|
+
_env: SiteEnv | None = None
|
217
|
+
|
218
|
+
|
219
|
+
def get_env():
|
220
|
+
"""Get a reference to the global `SiteEnv` object, creating it if necessary.
|
221
|
+
"""
|
222
|
+
global _env
|
223
|
+
if not _env:
|
224
|
+
_env = SiteEnv()
|
225
|
+
return _env
|
226
|
+
|
227
|
+
|
228
|
+
def update_env(args: Namespace | None = None):
|
229
|
+
"""Update the global `SiteEnv` object, optionally using arguments passed at the command-line to override values
|
230
|
+
passed through environmental variables.
|
231
|
+
"""
|
232
|
+
global _env
|
233
|
+
_env = SiteEnv(args)
|
234
|
+
|
235
|
+
|
236
|
+
def get_env_kwargs():
|
237
|
+
"""Get a dict of common kwargs for the environment
|
238
|
+
"""
|
239
|
+
return get_env().kwargs
|
@@ -0,0 +1,53 @@
|
|
1
|
+
"""
|
2
|
+
# get.py
|
3
|
+
|
4
|
+
This module defines the various webpages (the "GET" methods) provided by the website, connecting them to relevant
|
5
|
+
functions to return rendered templates.
|
6
|
+
"""
|
7
|
+
|
8
|
+
|
9
|
+
from flask import Flask, render_template
|
10
|
+
|
11
|
+
from psdi_data_conversion.database import get_database_path
|
12
|
+
from psdi_data_conversion.gui.env import get_env_kwargs
|
13
|
+
|
14
|
+
|
15
|
+
def index():
|
16
|
+
"""Return the web page along with relevant data
|
17
|
+
"""
|
18
|
+
return render_template("index.htm",
|
19
|
+
**get_env_kwargs())
|
20
|
+
|
21
|
+
|
22
|
+
def documentation():
|
23
|
+
"""Return the documentation page
|
24
|
+
"""
|
25
|
+
return render_template("documentation.htm",
|
26
|
+
**get_env_kwargs())
|
27
|
+
|
28
|
+
|
29
|
+
def database():
|
30
|
+
"""Return the raw database JSON file
|
31
|
+
"""
|
32
|
+
return open(get_database_path(), "r").read()
|
33
|
+
|
34
|
+
|
35
|
+
def report():
|
36
|
+
"""Return the report page
|
37
|
+
"""
|
38
|
+
return render_template("report.htm",
|
39
|
+
**get_env_kwargs())
|
40
|
+
|
41
|
+
|
42
|
+
def init_get(app: Flask):
|
43
|
+
"""Connect the provided Flask app to each of the pages on the site
|
44
|
+
"""
|
45
|
+
|
46
|
+
app.route('/')(index)
|
47
|
+
app.route('/index.htm')(index)
|
48
|
+
|
49
|
+
app.route('/documentation.htm')(documentation)
|
50
|
+
|
51
|
+
app.route('/report.htm')(report)
|
52
|
+
|
53
|
+
app.route('/database/')(database)
|
@@ -0,0 +1,176 @@
|
|
1
|
+
"""
|
2
|
+
# post.py
|
3
|
+
|
4
|
+
This module defines the various web addresses which do something (the "POST" methods) provided by the website,
|
5
|
+
connecting them to relevant functions.
|
6
|
+
"""
|
7
|
+
|
8
|
+
|
9
|
+
import json
|
10
|
+
import os
|
11
|
+
import sys
|
12
|
+
from multiprocessing import Lock
|
13
|
+
from traceback import format_exc
|
14
|
+
|
15
|
+
from flask import Flask, Response, abort, request
|
16
|
+
from werkzeug.utils import secure_filename
|
17
|
+
|
18
|
+
from psdi_data_conversion import constants as const
|
19
|
+
from psdi_data_conversion import log_utility
|
20
|
+
from psdi_data_conversion.converter import run_converter
|
21
|
+
from psdi_data_conversion.database import get_format_info
|
22
|
+
from psdi_data_conversion.file_io import split_archive_ext
|
23
|
+
from psdi_data_conversion.gui.env import get_env
|
24
|
+
|
25
|
+
# Key for the label given to the file uploaded in the web interface
|
26
|
+
FILE_TO_UPLOAD_KEY = 'fileToUpload'
|
27
|
+
|
28
|
+
# A lock to prevent multiple threads from logging at the same time
|
29
|
+
logLock = Lock()
|
30
|
+
|
31
|
+
|
32
|
+
def convert():
|
33
|
+
"""Convert file to a different format and save to folder 'downloads'. Delete original file. Note that downloading is
|
34
|
+
achieved in format.js
|
35
|
+
"""
|
36
|
+
|
37
|
+
env = get_env()
|
38
|
+
|
39
|
+
# Make sure the upload directory exists
|
40
|
+
os.makedirs(const.DEFAULT_INPUT_DIR, exist_ok=True)
|
41
|
+
|
42
|
+
file = request.files[FILE_TO_UPLOAD_KEY]
|
43
|
+
filename = secure_filename(file.filename)
|
44
|
+
|
45
|
+
qualified_filename = os.path.join(const.DEFAULT_INPUT_DIR, filename)
|
46
|
+
file.save(qualified_filename)
|
47
|
+
qualified_output_log = os.path.join(const.DEFAULT_OUTPUT_DIR,
|
48
|
+
split_archive_ext(filename)[0] + const.OUTPUT_LOG_EXT)
|
49
|
+
|
50
|
+
# Determine the input and output formats
|
51
|
+
d_formats = {}
|
52
|
+
for format_label in "to", "from":
|
53
|
+
name = request.form[format_label]
|
54
|
+
full_note = request.form[format_label+"_full"]
|
55
|
+
|
56
|
+
l_possible_formats = get_format_info(name, which="all")
|
57
|
+
|
58
|
+
# If there's only one possible format, use that
|
59
|
+
if len(l_possible_formats) == 1:
|
60
|
+
d_formats[format_label] = l_possible_formats[0]
|
61
|
+
continue
|
62
|
+
|
63
|
+
# Otherwise, find the format with the matching note
|
64
|
+
for possible_format in l_possible_formats:
|
65
|
+
if possible_format.note in full_note:
|
66
|
+
d_formats[format_label] = possible_format
|
67
|
+
break
|
68
|
+
else:
|
69
|
+
print(f"Format '{name}' with full description '{full_note}' could not be found in database.",
|
70
|
+
file=sys.stderr)
|
71
|
+
abort(const.STATUS_CODE_GENERAL)
|
72
|
+
|
73
|
+
if (not env.service_mode) or (request.form['token'] == env.token and env.token != ''):
|
74
|
+
try:
|
75
|
+
conversion_output = run_converter(name=request.form['converter'],
|
76
|
+
filename=qualified_filename,
|
77
|
+
data=request.form,
|
78
|
+
to_format=d_formats["to"],
|
79
|
+
from_format=d_formats["from"],
|
80
|
+
strict=(request.form['check_ext'] != "false"),
|
81
|
+
log_mode=env.log_mode,
|
82
|
+
log_level=env.log_level,
|
83
|
+
delete_input=True,
|
84
|
+
abort_callback=abort)
|
85
|
+
except Exception as e:
|
86
|
+
|
87
|
+
# Check for anticipated exceptions, and write a simpler message for them
|
88
|
+
for err_message in (const.ERR_CONVERSION_FAILED, const.ERR_CONVERTER_NOT_RECOGNISED,
|
89
|
+
const.ERR_EMPTY_ARCHIVE, const.ERR_WRONG_EXTENSIONS):
|
90
|
+
if log_utility.string_with_placeholders_matches(err_message, str(e)):
|
91
|
+
with open(qualified_output_log, "w") as fo:
|
92
|
+
fo.write(str(e))
|
93
|
+
abort(const.STATUS_CODE_GENERAL)
|
94
|
+
|
95
|
+
# If the exception provides a status code, get it
|
96
|
+
status_code: int
|
97
|
+
if hasattr(e, "status_code"):
|
98
|
+
status_code = e.status_code
|
99
|
+
else:
|
100
|
+
status_code = const.STATUS_CODE_GENERAL
|
101
|
+
|
102
|
+
# If the exception provides a message, report it
|
103
|
+
if hasattr(e, "message"):
|
104
|
+
msg = f"An unexpected exception was raised by the converter, with error message:\n{e.message}\n"
|
105
|
+
else:
|
106
|
+
# Failsafe exception message
|
107
|
+
msg = ("The following unexpected exception was raised by the converter:\n" +
|
108
|
+
format_exc()+"\n")
|
109
|
+
with open(qualified_output_log, "w") as fo:
|
110
|
+
fo.write(msg)
|
111
|
+
abort(status_code)
|
112
|
+
|
113
|
+
return repr(conversion_output)
|
114
|
+
else:
|
115
|
+
# return http status code 405
|
116
|
+
abort(405)
|
117
|
+
|
118
|
+
|
119
|
+
def feedback():
|
120
|
+
"""Take feedback data from the web app and log it
|
121
|
+
"""
|
122
|
+
|
123
|
+
try:
|
124
|
+
|
125
|
+
entry = {
|
126
|
+
"datetime": log_utility.get_date_time(),
|
127
|
+
}
|
128
|
+
|
129
|
+
report = json.loads(request.form['data'])
|
130
|
+
|
131
|
+
for key in ["type", "missing", "reason", "from", "to"]:
|
132
|
+
if key in report:
|
133
|
+
entry[key] = str(report[key])
|
134
|
+
|
135
|
+
# Write data in JSON format and send to stdout
|
136
|
+
logLock.acquire()
|
137
|
+
sys.stdout.write(f"{json.dumps(entry) + '\n'}")
|
138
|
+
logLock.release()
|
139
|
+
|
140
|
+
return Response(status=201)
|
141
|
+
|
142
|
+
except Exception:
|
143
|
+
|
144
|
+
return Response(status=400)
|
145
|
+
|
146
|
+
|
147
|
+
def delete():
|
148
|
+
"""Delete files in folder 'downloads'
|
149
|
+
"""
|
150
|
+
|
151
|
+
realbase = os.path.realpath(const.DEFAULT_OUTPUT_DIR)
|
152
|
+
|
153
|
+
realfilename = os.path.realpath(os.path.join(const.DEFAULT_OUTPUT_DIR, request.form['filename']))
|
154
|
+
reallogname = os.path.realpath(os.path.join(const.DEFAULT_OUTPUT_DIR, request.form['logname']))
|
155
|
+
|
156
|
+
if realfilename.startswith(realbase + os.sep) and reallogname.startswith(realbase + os.sep):
|
157
|
+
|
158
|
+
os.remove(realfilename)
|
159
|
+
os.remove(reallogname)
|
160
|
+
|
161
|
+
return 'okay'
|
162
|
+
|
163
|
+
else:
|
164
|
+
|
165
|
+
return Response(status=400)
|
166
|
+
|
167
|
+
|
168
|
+
def init_post(app: Flask):
|
169
|
+
"""Connect the provided Flask app to each of the post methods
|
170
|
+
"""
|
171
|
+
|
172
|
+
app.route('/convert/', methods=["POST"])(convert)
|
173
|
+
|
174
|
+
app.route('/feedback/', methods=["POST"])(feedback)
|
175
|
+
|
176
|
+
app.route('/delete/', methods=["POST"])(delete)
|
@@ -0,0 +1,102 @@
|
|
1
|
+
"""
|
2
|
+
# setup.py
|
3
|
+
|
4
|
+
This module handles setting up the Flask app
|
5
|
+
"""
|
6
|
+
|
7
|
+
|
8
|
+
import os
|
9
|
+
from collections.abc import Callable
|
10
|
+
from functools import wraps
|
11
|
+
from typing import Any
|
12
|
+
|
13
|
+
import werkzeug
|
14
|
+
from flask import Flask, cli
|
15
|
+
|
16
|
+
import psdi_data_conversion
|
17
|
+
from psdi_data_conversion import constants as const
|
18
|
+
from psdi_data_conversion.gui.accessibility import init_accessibility
|
19
|
+
from psdi_data_conversion.gui.env import get_env
|
20
|
+
from psdi_data_conversion.gui.get import init_get
|
21
|
+
from psdi_data_conversion.gui.post import init_post
|
22
|
+
|
23
|
+
_app: Flask | None = None
|
24
|
+
|
25
|
+
|
26
|
+
def _patch_flask_warning():
|
27
|
+
"""Monkey-patch Flask to disable the warnings that would otherwise appear for this so they don't confuse the user
|
28
|
+
"""
|
29
|
+
|
30
|
+
def suppress_warning(func: Callable[..., Any]) -> Callable[..., Any]:
|
31
|
+
@wraps(func)
|
32
|
+
def wrapper(*args, **kwargs) -> Any:
|
33
|
+
if args and isinstance(args[0], str) and args[0].startswith('WARNING: This is a development server.'):
|
34
|
+
return ''
|
35
|
+
return func(*args, **kwargs)
|
36
|
+
return wrapper
|
37
|
+
|
38
|
+
werkzeug.serving._ansi_style = suppress_warning(werkzeug.serving._ansi_style)
|
39
|
+
cli.show_server_banner = lambda *_: None
|
40
|
+
|
41
|
+
|
42
|
+
def _init_app():
|
43
|
+
"""Create and return the Flask app with appropriate settings"""
|
44
|
+
|
45
|
+
# Suppress Flask's warning, since we're using the dev server as a GUI
|
46
|
+
_patch_flask_warning()
|
47
|
+
|
48
|
+
app = Flask(const.APP_NAME)
|
49
|
+
|
50
|
+
# Set the file upload limit based on env vars
|
51
|
+
limit_upload_size(app)
|
52
|
+
|
53
|
+
# Connect the app to the various pages and methods of the website
|
54
|
+
init_get(app)
|
55
|
+
init_post(app)
|
56
|
+
init_accessibility(app)
|
57
|
+
|
58
|
+
return app
|
59
|
+
|
60
|
+
|
61
|
+
def limit_upload_size(app: Flask | None = None):
|
62
|
+
"""Impose a limit on the maximum file that can be uploaded before Flask will raise an error"""
|
63
|
+
|
64
|
+
if app is None:
|
65
|
+
app = get_app()
|
66
|
+
|
67
|
+
env = get_env()
|
68
|
+
|
69
|
+
# Determine the largest possible file size that can be uploaded, keeping in mind that 0 indicates unlimited
|
70
|
+
larger_max_file_size = env.max_file_size
|
71
|
+
if (env.max_file_size > 0) and (env.max_file_size_ob > env.max_file_size):
|
72
|
+
larger_max_file_size = env.max_file_size_ob
|
73
|
+
|
74
|
+
if larger_max_file_size > 0:
|
75
|
+
app.config['MAX_CONTENT_LENGTH'] = larger_max_file_size
|
76
|
+
else:
|
77
|
+
app.config['MAX_CONTENT_LENGTH'] = None
|
78
|
+
|
79
|
+
|
80
|
+
def get_app() -> Flask:
|
81
|
+
"""Get a reference to the global `Flask` app, creating it if necessary.
|
82
|
+
"""
|
83
|
+
global _app
|
84
|
+
if not _app:
|
85
|
+
_app = _init_app()
|
86
|
+
return _app
|
87
|
+
|
88
|
+
|
89
|
+
def start_app():
|
90
|
+
"""Start the Flask app - this requires being run from the base directory of the project, so this changes the
|
91
|
+
current directory to there. Anything else which changes it while the app is running may interfere with its proper
|
92
|
+
execution.
|
93
|
+
"""
|
94
|
+
|
95
|
+
old_cwd = os.getcwd()
|
96
|
+
|
97
|
+
try:
|
98
|
+
os.chdir(os.path.join(psdi_data_conversion.__path__[0], ".."))
|
99
|
+
get_app().run(debug=get_env().debug_mode)
|
100
|
+
finally:
|
101
|
+
# Return to the previous directory after running the app
|
102
|
+
os.chdir(old_cwd)
|