psdi-data-conversion 0.0.23__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.
Files changed (81) hide show
  1. psdi_data_conversion/__init__.py +11 -0
  2. psdi_data_conversion/app.py +242 -0
  3. psdi_data_conversion/bin/linux/atomsk +0 -0
  4. psdi_data_conversion/bin/linux/c2x +0 -0
  5. psdi_data_conversion/bin/mac/atomsk +0 -0
  6. psdi_data_conversion/bin/mac/c2x +0 -0
  7. psdi_data_conversion/constants.py +185 -0
  8. psdi_data_conversion/converter.py +459 -0
  9. psdi_data_conversion/converters/__init__.py +6 -0
  10. psdi_data_conversion/converters/atomsk.py +32 -0
  11. psdi_data_conversion/converters/base.py +702 -0
  12. psdi_data_conversion/converters/c2x.py +32 -0
  13. psdi_data_conversion/converters/openbabel.py +239 -0
  14. psdi_data_conversion/database.py +1064 -0
  15. psdi_data_conversion/dist.py +87 -0
  16. psdi_data_conversion/file_io.py +216 -0
  17. psdi_data_conversion/log_utility.py +241 -0
  18. psdi_data_conversion/main.py +776 -0
  19. psdi_data_conversion/scripts/atomsk.sh +32 -0
  20. psdi_data_conversion/scripts/c2x.sh +26 -0
  21. psdi_data_conversion/security.py +38 -0
  22. psdi_data_conversion/static/content/accessibility.htm +254 -0
  23. psdi_data_conversion/static/content/convert.htm +121 -0
  24. psdi_data_conversion/static/content/convertato.htm +65 -0
  25. psdi_data_conversion/static/content/convertc2x.htm +65 -0
  26. psdi_data_conversion/static/content/documentation.htm +94 -0
  27. psdi_data_conversion/static/content/feedback.htm +53 -0
  28. psdi_data_conversion/static/content/header-links.html +8 -0
  29. psdi_data_conversion/static/content/index-versions/header-links.html +8 -0
  30. psdi_data_conversion/static/content/index-versions/psdi-common-footer.html +99 -0
  31. psdi_data_conversion/static/content/index-versions/psdi-common-header.html +28 -0
  32. psdi_data_conversion/static/content/psdi-common-footer.html +99 -0
  33. psdi_data_conversion/static/content/psdi-common-header.html +28 -0
  34. psdi_data_conversion/static/content/report.htm +103 -0
  35. psdi_data_conversion/static/data/data.json +143940 -0
  36. psdi_data_conversion/static/img/colormode-toggle-dm.svg +3 -0
  37. psdi_data_conversion/static/img/colormode-toggle-lm.svg +3 -0
  38. psdi_data_conversion/static/img/psdi-icon-dark.svg +136 -0
  39. psdi_data_conversion/static/img/psdi-icon-light.svg +208 -0
  40. psdi_data_conversion/static/img/psdi-logo-darktext.png +0 -0
  41. psdi_data_conversion/static/img/psdi-logo-lighttext.png +0 -0
  42. psdi_data_conversion/static/img/social-logo-bluesky-black.svg +4 -0
  43. psdi_data_conversion/static/img/social-logo-bluesky-white.svg +4 -0
  44. psdi_data_conversion/static/img/social-logo-instagram-black.svg +1 -0
  45. psdi_data_conversion/static/img/social-logo-instagram-white.svg +1 -0
  46. psdi_data_conversion/static/img/social-logo-linkedin-black.png +0 -0
  47. psdi_data_conversion/static/img/social-logo-linkedin-white.png +0 -0
  48. psdi_data_conversion/static/img/social-logo-mastodon-black.svg +4 -0
  49. psdi_data_conversion/static/img/social-logo-mastodon-white.svg +4 -0
  50. psdi_data_conversion/static/img/social-logo-x-black.svg +3 -0
  51. psdi_data_conversion/static/img/social-logo-x-white.svg +3 -0
  52. psdi_data_conversion/static/img/social-logo-youtube-black.png +0 -0
  53. psdi_data_conversion/static/img/social-logo-youtube-white.png +0 -0
  54. psdi_data_conversion/static/img/ukri-epsr-logo-darktext.png +0 -0
  55. psdi_data_conversion/static/img/ukri-epsr-logo-lighttext.png +0 -0
  56. psdi_data_conversion/static/img/ukri-logo-darktext.png +0 -0
  57. psdi_data_conversion/static/img/ukri-logo-lighttext.png +0 -0
  58. psdi_data_conversion/static/javascript/accessibility.js +196 -0
  59. psdi_data_conversion/static/javascript/common.js +42 -0
  60. psdi_data_conversion/static/javascript/convert.js +296 -0
  61. psdi_data_conversion/static/javascript/convert_common.js +252 -0
  62. psdi_data_conversion/static/javascript/convertato.js +107 -0
  63. psdi_data_conversion/static/javascript/convertc2x.js +107 -0
  64. psdi_data_conversion/static/javascript/data.js +176 -0
  65. psdi_data_conversion/static/javascript/format.js +611 -0
  66. psdi_data_conversion/static/javascript/load_accessibility.js +89 -0
  67. psdi_data_conversion/static/javascript/psdi-common.js +177 -0
  68. psdi_data_conversion/static/javascript/report.js +381 -0
  69. psdi_data_conversion/static/styles/format.css +147 -0
  70. psdi_data_conversion/static/styles/psdi-common.css +705 -0
  71. psdi_data_conversion/templates/index.htm +114 -0
  72. psdi_data_conversion/testing/__init__.py +5 -0
  73. psdi_data_conversion/testing/constants.py +12 -0
  74. psdi_data_conversion/testing/conversion_callbacks.py +394 -0
  75. psdi_data_conversion/testing/conversion_test_specs.py +208 -0
  76. psdi_data_conversion/testing/utils.py +522 -0
  77. psdi_data_conversion-0.0.23.dist-info/METADATA +663 -0
  78. psdi_data_conversion-0.0.23.dist-info/RECORD +81 -0
  79. psdi_data_conversion-0.0.23.dist-info/WHEEL +4 -0
  80. psdi_data_conversion-0.0.23.dist-info/entry_points.txt +2 -0
  81. psdi_data_conversion-0.0.23.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,11 @@
1
+ """
2
+ # PSDI Data Conversion package
3
+
4
+ This is the primary package for the PSDI Data Conversion python package, including all library code and executable
5
+ scripts used for it.
6
+
7
+ The README for this project can be found either packaged alongside it or online at
8
+ https://github.com/PSDI-UK/psdi-data-conversion/blob/main/README.md
9
+
10
+ The documentation for this package is still WIP as we prepare for release.
11
+ """
@@ -0,0 +1,242 @@
1
+ """app.py
2
+
3
+ Version 1.0, 8th November 2024
4
+
5
+ This script acts as a server for the PSDI Data Conversion Service website.
6
+ """
7
+
8
+ import hashlib
9
+ import os
10
+ import json
11
+ from datetime import datetime
12
+ from subprocess import run
13
+ import sys
14
+ import traceback
15
+ from flask import Flask, request, render_template, abort, Response
16
+
17
+ from psdi_data_conversion import log_utility
18
+ from psdi_data_conversion import constants as const
19
+ from psdi_data_conversion.converter import run_converter
20
+ from psdi_data_conversion.file_io import split_archive_ext
21
+
22
+ # Env var for the SHA of the latest commit
23
+ SHA_EV = "SHA"
24
+
25
+ # Env var for whether this is running in service mode or locally
26
+ SERVICE_MODE_EV = "SERVICE_MODE"
27
+
28
+ # Env var for whether this is a production release or development
29
+ PRODUCTION_EV = "PRODUCTION_MODE"
30
+
31
+ # Key for the label given to the file uploaded in the web interface
32
+ FILE_TO_UPLOAD_KEY = 'fileToUpload'
33
+
34
+ # Create a token by hashing the current date and time.
35
+ dt = str(datetime.now())
36
+ token = hashlib.md5(dt.encode('utf8')).hexdigest()
37
+
38
+ # Get the service and production modes from their envvars
39
+ service_mode_ev = os.environ.get(SERVICE_MODE_EV)
40
+ service_mode = (service_mode_ev is not None) and (service_mode_ev.lower() == "true")
41
+ production_mode_ev = os.environ.get(PRODUCTION_EV)
42
+ production_mode = (production_mode_ev is not None) and (production_mode_ev.lower() == "true")
43
+
44
+ # Get the logging mode and level from their envvars
45
+ ev_log_mode = os.environ.get(const.LOG_MODE_EV)
46
+ if ev_log_mode is None:
47
+ log_mode = const.LOG_MODE_DEFAULT
48
+ else:
49
+ ev_log_mode = ev_log_mode.lower()
50
+ if ev_log_mode not in const.L_ALLOWED_LOG_MODES:
51
+ print(f"ERROR: Unrecognised logging option: {ev_log_mode}. Allowed options are: {const.L_ALLOWED_LOG_MODES}",
52
+ file=sys.stderr)
53
+ exit(1)
54
+ log_mode = ev_log_mode
55
+
56
+ ev_log_level = os.environ.get(const.LOG_LEVEL_EV)
57
+ if ev_log_level is None:
58
+ log_level = None
59
+ else:
60
+ try:
61
+ log_level = log_utility.get_log_level_from_str(ev_log_level)
62
+ except ValueError as e:
63
+ print(f"ERROR: {str(e)}")
64
+ exit(1)
65
+
66
+ app = Flask(__name__)
67
+
68
+
69
+ def get_last_sha() -> str:
70
+ """Get the SHA of the last commit
71
+ """
72
+
73
+ # First check if the SHA is provided through an environmental variable
74
+ ev_sha = os.environ.get(SHA_EV)
75
+ if ev_sha:
76
+ return ev_sha
77
+
78
+ try:
79
+ # This bash command calls `git log` to get info on the last commit, uses `head` to trim it to one line, then
80
+ # uses `gawk` to get just the second word of this line, which is the SHA of this commit
81
+ cmd = "git log -n 1 | head -n 1 | gawk '{print($2)}'"
82
+
83
+ out_bytes = run(cmd, shell=True, capture_output=True).stdout
84
+ out_str = str(out_bytes.decode()).strip()
85
+
86
+ except Exception:
87
+ print("ERROR: Could not determine SHA of most recent commit. Error was:\n" + traceback.format_exc(),
88
+ file=sys.stderr)
89
+ out_str = "N/A"
90
+
91
+ return out_str
92
+
93
+
94
+ @app.route('/')
95
+ def website():
96
+ """Return the web page along with the token
97
+ """
98
+ # Get the maximum allowed size from the envvar for it
99
+ ev_max_file_size = os.environ.get(const.MAX_FILESIZE_EV)
100
+ if ev_max_file_size is not None:
101
+ max_file_size = float(ev_max_file_size)*const.MEGABYTE
102
+ else:
103
+ max_file_size = const.DEFAULT_MAX_FILE_SIZE
104
+
105
+ data = [{'token': token,
106
+ 'max_file_size': max_file_size,
107
+ 'service_mode': service_mode,
108
+ 'production_mode': production_mode,
109
+ 'sha': get_last_sha()}]
110
+ return render_template("index.htm", data=data)
111
+
112
+
113
+ @app.route('/convert/', methods=['POST'])
114
+ def convert():
115
+ """Convert file to a different format and save to folder 'downloads'. Delete original file. Note that downloading is
116
+ achieved in format.js
117
+ """
118
+
119
+ # Make sure the upload directory exists
120
+ os.makedirs(const.DEFAULT_UPLOAD_DIR, exist_ok=True)
121
+
122
+ # Save the file in the upload directory
123
+ file = request.files[FILE_TO_UPLOAD_KEY]
124
+ filename = filename = file.filename
125
+
126
+ qualified_filename = os.path.join(const.DEFAULT_UPLOAD_DIR, filename)
127
+ file.save(qualified_filename)
128
+ qualified_output_log = os.path.join(const.DEFAULT_DOWNLOAD_DIR,
129
+ split_archive_ext(filename)[0] + const.OUTPUT_LOG_EXT)
130
+
131
+ if (not service_mode) or (request.form['token'] == token and token != ''):
132
+ try:
133
+ conversion_output = run_converter(name=request.form['converter'],
134
+ filename=qualified_filename,
135
+ data=request.form,
136
+ to_format=request.form['to'],
137
+ from_format=request.form['from'],
138
+ strict=request.form['check_ext'],
139
+ log_mode=log_mode,
140
+ log_level=log_level,
141
+ delete_input=True,
142
+ abort_callback=abort)
143
+ except Exception as e:
144
+
145
+ # Check for anticipated exceptions, and write a simpler message for them
146
+ for err_message in (const.ERR_CONVERSION_FAILED, const.ERR_CONVERTER_NOT_RECOGNISED,
147
+ const.ERR_EMPTY_ARCHIVE, const.ERR_WRONG_EXTENSIONS):
148
+ if log_utility.string_with_placeholders_matches(err_message, str(e)):
149
+ with open(qualified_output_log, "w") as fo:
150
+ fo.write(str(e))
151
+ abort(const.STATUS_CODE_GENERAL)
152
+
153
+ # Failsafe exception message
154
+ msg = "The following unexpected exception was raised by the converter:\n" + traceback.format_exc()+"\n"
155
+ with open(qualified_output_log, "w") as fo:
156
+ fo.write(msg)
157
+ abort(const.STATUS_CODE_GENERAL)
158
+
159
+ return repr(conversion_output)
160
+ else:
161
+ # return http status code 405
162
+ abort(405)
163
+
164
+
165
+ @app.route('/feedback/', methods=['POST'])
166
+ def feedback():
167
+ """Take feedback data from the web app and log it
168
+ """
169
+
170
+ try:
171
+
172
+ entry = {
173
+ "datetime": log_utility.get_date_time(),
174
+ }
175
+
176
+ report = json.loads(request.form['data'])
177
+
178
+ for key in ["type", "missing", "reason", "from", "to"]:
179
+ if key in report:
180
+ entry[key] = str(report[key])
181
+
182
+ log_utility.append_to_log_file("feedback", entry)
183
+
184
+ return Response(status=201)
185
+
186
+ except Exception:
187
+
188
+ return Response(status=400)
189
+
190
+
191
+ @app.route('/delete/', methods=['POST'])
192
+ def delete():
193
+ """Delete files in folder 'downloads'
194
+ """
195
+
196
+ realbase = os.path.realpath(const.DEFAULT_DOWNLOAD_DIR)
197
+
198
+ realfilename = os.path.realpath(os.path.join(const.DEFAULT_DOWNLOAD_DIR, request.form['filename']))
199
+ reallogname = os.path.realpath(os.path.join(const.DEFAULT_DOWNLOAD_DIR, request.form['logname']))
200
+
201
+ if realfilename.startswith(realbase + os.sep) and reallogname.startswith(realbase + os.sep):
202
+
203
+ os.remove(realfilename)
204
+ os.remove(reallogname)
205
+
206
+ return 'okay'
207
+
208
+ else:
209
+
210
+ return Response(status=400)
211
+
212
+
213
+ @app.route('/del/', methods=['POST'])
214
+ def delete_file():
215
+ """Delete file (cURL)
216
+ """
217
+ os.remove(request.form['filepath'])
218
+ return 'Server-side file ' + request.form['filepath'] + ' deleted\n'
219
+
220
+
221
+ @app.route('/data/', methods=['GET'])
222
+ def data():
223
+ """Check that the incoming token matches the one sent to the user (should mostly prevent spambots). Write date- and
224
+ time-stamped user input to server-side file 'user_responses'.
225
+
226
+ $$$$$$$$$$ Retained in case direct logging is required in the future. $$$$$$$$$$
227
+
228
+ Returns
229
+ -------
230
+ str
231
+ Output status - 'okay' if exited successfuly
232
+ """
233
+ if service_mode and request.args['token'] == token and token != '':
234
+ message = '[' + log_utility.get_date_time() + '] ' + request.args['data'] + '\n'
235
+
236
+ with open("user_responses", "a") as f:
237
+ f.write(message)
238
+
239
+ return 'okay'
240
+ else:
241
+ # return http status code 405
242
+ abort(405)
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,185 @@
1
+ """@file psdi_data_conversion/constants.py
2
+
3
+ Created 2025-01-23 by Bryan Gillis.
4
+
5
+ Miscellaneous constant values used within this project.
6
+
7
+ These values are stored as constants rather than hardcoded literals for various reasons, including:
8
+ - Better assurance of consistency that the same value is used every time
9
+ - If a value needs to be changed, this only needs to be done at one location
10
+ - Compatibility with IDE features - a constant can be checked for validity by an IDE, while e.g. a string key for a dict
11
+ can't, allowing more errors to be caught and fixed by the IDE rather than at runtime
12
+ - The use of a constant may improve readability - e.g. `MEGABYTE = 1024*1024; max_file_size = 1*MEGABYTE` is more
13
+ readable than `max_file_size = 1*1024*1024`, and so doesn't require a comment like the latter would
14
+
15
+ There are some known drawbacks to this approach which need to be considered though:
16
+ - Constants may obscure readability - it may be quite relevant to the reader exactly what a constant represents, which
17
+ is obscured until they (at minimum) mouse over it
18
+ - More code is necessary to use a constant than a literal (at minimum it needs an extra line to define it, and when
19
+ stored here, it also needs a line to import it or this module)
20
+
21
+ With these drawbacks in mind, we make the following recommendations for constant use:
22
+ - Messages for the user (print/logging messages, exceptions) should by default not be stored as constants. They should
23
+ be made constants if it's necessary to reference their exact text elsewhere (either in the executable code or unit
24
+ tests). In this case, the name of the constant should be descriptive, even if this means a rather long name
25
+ - If a value is only used in one file and only likely to ever be used in that file, it can be defined as a constant
26
+ there (or if used only two or three times in quick succession, left as a literal)
27
+ - Of course, deviations from this should be made when necessary, such as to avoid circular imports
28
+ """
29
+
30
+ import logging
31
+ import shutil
32
+
33
+ # Interface
34
+ # ---------
35
+
36
+ # The name of the command-line script
37
+ CL_SCRIPT_NAME = "psdi-data-convert"
38
+
39
+ # Environmental variables
40
+ LOG_MODE_EV = "LOG_MODE"
41
+ LOG_LEVEL_EV = "LOG_LEVEL"
42
+ MAX_FILESIZE_EV = "MAX_FILESIZE"
43
+
44
+ # Files and Folders
45
+ # -----------------
46
+
47
+ # Maximum output file size in bytes
48
+ MEGABYTE = 1024*1024
49
+ DEFAULT_MAX_FILE_SIZE = 0*MEGABYTE
50
+
51
+ DEFAULT_UPLOAD_DIR = './psdi_data_conversion/static/uploads'
52
+ DEFAULT_DOWNLOAD_DIR = './psdi_data_conversion/static/downloads'
53
+
54
+ # Filename of the database, relative to the base of the python package
55
+ DATABASE_FILENAME = "static/data/data.json"
56
+
57
+ # Archive extensions and formats ('format' here meaning the value expected by shutil's archive functions)
58
+
59
+ ZIP_EXTENSION = ".zip"
60
+ ZIP_FORMAT = "zip"
61
+
62
+ D_ZIP_FORMATS = {ZIP_EXTENSION: ZIP_FORMAT}
63
+
64
+ TAR_EXTENSION = ".tar"
65
+ TAR_FORMAT = "tar"
66
+ GZTAR_EXTENSION = ".tar.gz"
67
+ GZTAR_FORMAT = "gztar"
68
+ BZTAR_EXTENSION = ".tar.bz"
69
+ BZTAR_FORMAT = "bztar"
70
+ XZTAR_EXTENSION = ".tar.xz"
71
+ XZTAR_FORMAT = "xztar"
72
+
73
+ D_TAR_FORMATS = {TAR_EXTENSION: TAR_FORMAT,
74
+ GZTAR_EXTENSION: GZTAR_FORMAT,
75
+ BZTAR_EXTENSION: BZTAR_FORMAT,
76
+ XZTAR_EXTENSION: BZTAR_FORMAT}
77
+
78
+ # A list of specifically the extensions that are combinations of multiple different extensions
79
+ L_COMPOUND_EXTENSIONS = [GZTAR_EXTENSION, BZTAR_EXTENSION, XZTAR_EXTENSION]
80
+
81
+ # Formats which are supported by shutil's built-in archive utility
82
+ D_SUPPORTED_ARCHIVE_FORMATS = {**D_ZIP_FORMATS, **D_TAR_FORMATS}
83
+
84
+ L_UNSUPPORTED_ARCHIVE_EXTENSIONS = [".rar", ".7z"]
85
+
86
+ L_ALL_ARCHIVE_EXTENSIONS = [*D_SUPPORTED_ARCHIVE_FORMATS.keys(), *L_UNSUPPORTED_ARCHIVE_EXTENSIONS]
87
+
88
+
89
+ # Logging and Formatting
90
+ # ----------------------
91
+
92
+ # Number of character spaces allocated for flags/options
93
+
94
+ # Get the terminal width so we can prettily print help text
95
+ TERM_WIDTH, _ = shutil.get_terminal_size((80, 20))
96
+
97
+ # Log formatting
98
+ LOG_FORMAT = r'[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s'
99
+ TIMESTAMP_FORMAT = r"%Y-%m-%d %H:%M:%S"
100
+
101
+ # Regex to match date/time format
102
+ DATE_RE_RAW = r"\d{4}-[0-1]\d-[0-3]\d"
103
+ TIME_RE_RAW = r"[0-2]\d:[0-5]\d:[0-5]\d"
104
+ DATETIME_RE_RAW = f"{DATE_RE_RAW} {TIME_RE_RAW}"
105
+
106
+ # Log mode info and settings
107
+ LOG_FULL = "full"
108
+ LOG_FULL_FORCE = "full-force"
109
+ LOG_SIMPLE = "simple"
110
+ LOG_STDOUT = "stdout"
111
+ LOG_NONE = "none"
112
+
113
+ LOG_MODE_DEFAULT = LOG_SIMPLE
114
+
115
+ L_ALLOWED_LOG_MODES = (LOG_FULL, LOG_FULL_FORCE, LOG_SIMPLE, LOG_STDOUT, LOG_NONE)
116
+
117
+ LOG_EXT = ".log"
118
+ OUTPUT_LOG_EXT = f"{LOG_EXT}.txt"
119
+
120
+ # Settings for global logger
121
+ GLOBAL_LOG_FILENAME = "./error_log.txt"
122
+ GLOBAL_LOGGER_LEVEL = logging.ERROR
123
+
124
+ # Settings for local logger
125
+ LOCAL_LOGGER_NAME = "data-conversion"
126
+ DEFAULT_LOCAL_LOGGER_LEVEL = logging.INFO
127
+ DEFAULT_LISTING_LOG_FILE = "data-convert-list" + LOG_EXT
128
+
129
+ # Converters and Related
130
+ # ----------------------
131
+
132
+ # Converter names are determined based on the modules present in the 'converters' package by the 'converter' module
133
+ # This module contains constant dicts and lists of registered converters
134
+
135
+ # Default converter - this must match the name of one of the registered converters
136
+ CONVERTER_DEFAULT = 'Open Babel'
137
+
138
+ # File format properties which are used to judge conversion quality - KEY is the key for it in the database, and LABEL
139
+ # is how we want to print it out for the user
140
+ QUAL_COMP_KEY = "composition"
141
+ QUAL_COMP_LABEL = "Atomic composition is"
142
+ QUAL_CONN_KEY = "connections"
143
+ QUAL_CONN_LABEL = "Atomic connections are"
144
+ QUAL_2D_KEY = "two_dim"
145
+ QUAL_2D_LABEL = "2D atomic coordinates are"
146
+ QUAL_3D_KEY = "three_dim"
147
+ QUAL_3D_LABEL = "2D atomic coordinates are"
148
+
149
+ D_QUAL_LABELS = {QUAL_COMP_KEY: QUAL_COMP_LABEL,
150
+ QUAL_CONN_KEY: QUAL_CONN_LABEL,
151
+ QUAL_2D_KEY: QUAL_2D_LABEL,
152
+ QUAL_3D_KEY: QUAL_3D_LABEL}
153
+
154
+ # Notes for conversion quality
155
+ QUAL_NOTE_IN_UNKNOWN = ("Potential data extrapolation: {} represented in the output format but its representation in "
156
+ "the input format is unknown")
157
+ QUAL_NOTE_OUT_UNKNOWN = ("Potential data loss: {} represented in the input format, but its representation in the "
158
+ "output format is unknown")
159
+ QUAL_NOTE_BOTH_UNKNOWN = ("Potential data loss or extrapolation: {} potentially not supported in either or both of "
160
+ "the input and output formats")
161
+ QUAL_NOTE_IN_MISSING = "Potential data extrapolation: {} represented in the output format but not the input format"
162
+ QUAL_NOTE_OUT_MISSING = "Potential data loss: {} represented in the input format but not the output format"
163
+
164
+ # Conversion quality strings
165
+ QUAL_UNKNOWN = 'unknown'
166
+ QUAL_VERYGOOD = 'very good'
167
+ QUAL_GOOD = 'good'
168
+ QUAL_OKAY = 'okay'
169
+ QUAL_POOR = 'poor'
170
+ QUAL_VERYPOOR = 'very poor'
171
+
172
+ # Errors
173
+ # ------
174
+
175
+ # HTTP status codes for various types of errors
176
+ STATUS_CODE_BAD_METHOD = 405
177
+ STATUS_CODE_SIZE = 413
178
+ STATUS_CODE_GENERAL = 500
179
+
180
+ # Error messages
181
+ ERR_CONVERTER_NOT_RECOGNISED = "Converter {} not recognized. Allowed converters are: "
182
+ ERR_WRONG_EXTENSIONS = "Input file '{file}' does not have expected extension '{ext}'"
183
+ ERR_EMPTY_ARCHIVE = "No files to convert were contained in archive"
184
+ ERR_CONVERSION_FAILED = ("File conversion failed for one or more files. Lines from the output log "
185
+ "{} which indicate possible sources of error: ")