psdi-data-conversion 0.0.35__py3-none-any.whl → 0.0.37__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 (39) hide show
  1. psdi_data_conversion/app.py +110 -10
  2. psdi_data_conversion/constants.py +2 -0
  3. psdi_data_conversion/converter.py +16 -6
  4. psdi_data_conversion/converters/atomsk.py +3 -1
  5. psdi_data_conversion/converters/base.py +99 -39
  6. psdi_data_conversion/converters/c2x.py +3 -1
  7. psdi_data_conversion/converters/openbabel.py +40 -1
  8. psdi_data_conversion/database.py +5 -0
  9. psdi_data_conversion/main.py +18 -10
  10. psdi_data_conversion/static/content/accessibility.htm +5 -5
  11. psdi_data_conversion/static/content/convert.htm +18 -13
  12. psdi_data_conversion/static/content/convertato.htm +40 -33
  13. psdi_data_conversion/static/content/convertc2x.htm +40 -33
  14. psdi_data_conversion/static/content/documentation.htm +4 -4
  15. psdi_data_conversion/static/content/download.htm +26 -10
  16. psdi_data_conversion/static/content/feedback.htm +4 -4
  17. psdi_data_conversion/static/content/index-versions/psdi-common-header.html +1 -1
  18. psdi_data_conversion/static/content/psdi-common-header.html +1 -1
  19. psdi_data_conversion/static/content/report.htm +9 -7
  20. psdi_data_conversion/static/javascript/common.js +20 -0
  21. psdi_data_conversion/static/javascript/convert.js +1 -2
  22. psdi_data_conversion/static/javascript/convert_common.js +80 -7
  23. psdi_data_conversion/static/javascript/convertato.js +1 -2
  24. psdi_data_conversion/static/javascript/convertc2x.js +1 -2
  25. psdi_data_conversion/static/javascript/format.js +12 -0
  26. psdi_data_conversion/static/javascript/report.js +6 -0
  27. psdi_data_conversion/static/styles/format.css +0 -6
  28. psdi_data_conversion/static/styles/psdi-common.css +10 -6
  29. psdi_data_conversion/templates/index.htm +10 -2
  30. psdi_data_conversion/testing/constants.py +6 -3
  31. psdi_data_conversion/testing/conversion_callbacks.py +7 -6
  32. psdi_data_conversion/testing/conversion_test_specs.py +333 -153
  33. psdi_data_conversion/testing/gui.py +366 -0
  34. psdi_data_conversion/testing/utils.py +108 -51
  35. {psdi_data_conversion-0.0.35.dist-info → psdi_data_conversion-0.0.37.dist-info}/METADATA +90 -51
  36. {psdi_data_conversion-0.0.35.dist-info → psdi_data_conversion-0.0.37.dist-info}/RECORD +39 -38
  37. {psdi_data_conversion-0.0.35.dist-info → psdi_data_conversion-0.0.37.dist-info}/WHEEL +1 -1
  38. {psdi_data_conversion-0.0.35.dist-info → psdi_data_conversion-0.0.37.dist-info}/entry_points.txt +1 -0
  39. {psdi_data_conversion-0.0.35.dist-info → psdi_data_conversion-0.0.37.dist-info}/licenses/LICENSE +0 -0
@@ -5,6 +5,7 @@ Version 1.0, 8th November 2024
5
5
  This script acts as a server for the PSDI Data Conversion Service website.
6
6
  """
7
7
 
8
+ from argparse import ArgumentParser
8
9
  import hashlib
9
10
  import os
10
11
  import json
@@ -14,6 +15,7 @@ import sys
14
15
  import traceback
15
16
  from flask import Flask, request, render_template, abort, Response
16
17
 
18
+ import psdi_data_conversion
17
19
  from psdi_data_conversion import log_utility
18
20
  from psdi_data_conversion import constants as const
19
21
  from psdi_data_conversion.converter import run_converter
@@ -63,6 +65,20 @@ else:
63
65
  print(f"ERROR: {str(e)}")
64
66
  exit(1)
65
67
 
68
+ # Get the maximum allowed size from the envvar for it
69
+ ev_max_file_size = os.environ.get(const.MAX_FILESIZE_EV)
70
+ if ev_max_file_size is not None:
71
+ max_file_size = float(ev_max_file_size)*const.MEGABYTE
72
+ else:
73
+ max_file_size = const.DEFAULT_MAX_FILE_SIZE
74
+
75
+ # And same for the Open Babel maximum file size
76
+ ev_max_file_size_ob = os.environ.get(const.MAX_FILESIZE_OB_EV)
77
+ if ev_max_file_size_ob is not None:
78
+ max_file_size_ob = float(ev_max_file_size_ob)*const.MEGABYTE
79
+ else:
80
+ max_file_size_ob = const.DEFAULT_MAX_FILE_SIZE_OB
81
+
66
82
  app = Flask(__name__)
67
83
 
68
84
 
@@ -95,15 +111,10 @@ def get_last_sha() -> str:
95
111
  def website():
96
112
  """Return the web page along with the token
97
113
  """
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
114
 
105
115
  data = [{'token': token,
106
116
  'max_file_size': max_file_size,
117
+ 'max_file_size_ob': max_file_size_ob,
107
118
  'service_mode': service_mode,
108
119
  'production_mode': production_mode,
109
120
  'sha': get_last_sha()}]
@@ -135,7 +146,7 @@ def convert():
135
146
  data=request.form,
136
147
  to_format=request.form['to'],
137
148
  from_format=request.form['from'],
138
- strict=request.form['check_ext'],
149
+ strict=(request.form['check_ext'] != "false"),
139
150
  log_mode=log_mode,
140
151
  log_level=log_level,
141
152
  delete_input=True,
@@ -150,11 +161,23 @@ def convert():
150
161
  fo.write(str(e))
151
162
  abort(const.STATUS_CODE_GENERAL)
152
163
 
153
- # Failsafe exception message
154
- msg = "The following unexpected exception was raised by the converter:\n" + traceback.format_exc()+"\n"
164
+ # If the exception provides a status code, get it
165
+ status_code: int
166
+ if hasattr(e, "status_code"):
167
+ status_code = e.status_code
168
+ else:
169
+ status_code = const.STATUS_CODE_GENERAL
170
+
171
+ # If the exception provides a message, report it
172
+ if hasattr(e, "message"):
173
+ msg = f"An unexpected exception was raised by the converter, with error message:\n{e.message}\n"
174
+ else:
175
+ # Failsafe exception message
176
+ msg = ("The following unexpected exception was raised by the converter:\n" +
177
+ traceback.format_exc()+"\n")
155
178
  with open(qualified_output_log, "w") as fo:
156
179
  fo.write(msg)
157
- abort(const.STATUS_CODE_GENERAL)
180
+ abort(status_code)
158
181
 
159
182
  return repr(conversion_output)
160
183
  else:
@@ -240,3 +263,80 @@ def data():
240
263
  else:
241
264
  # return http status code 405
242
265
  abort(405)
266
+
267
+
268
+ def start_app():
269
+ """Start the Flask app - this requires being run from the base directory of the project, so this changes the
270
+ current directory to there. Anything else which changes it while the app is running may interfere with its proper
271
+ execution.
272
+ """
273
+
274
+ os.chdir(os.path.join(psdi_data_conversion.__path__[0], ".."))
275
+ app.run()
276
+
277
+
278
+ def main():
279
+ """Standard entry-point function for this script.
280
+ """
281
+
282
+ parser = ArgumentParser()
283
+
284
+ parser.add_argument("--use-env-vars", action="store_true",
285
+ help="If set, all other arguments and defaults for this script are ignored, and environmental "
286
+ "variables and their defaults will instead control execution. These defaults will result in "
287
+ "the app running in production server mode.")
288
+
289
+ parser.add_argument("--max-file-size", type=float, default=const.DEFAULT_MAX_FILE_SIZE,
290
+ help="The maximum allowed filesize in MB - 0 (default) indicates no maximum")
291
+
292
+ parser.add_argument("--max-file-size-ob", type=float, default=const.DEFAULT_MAX_FILE_SIZE_OB,
293
+ help="The maximum allowed filesize in MB for the Open Babel converter, taking precendence over "
294
+ "the general maximum file size when Open Babel is used - 0 indicates no maximum. Default 1 MB.")
295
+
296
+ parser.add_argument("--service-mode", action="store_true",
297
+ help="If set, will run as if deploying a service rather than the local GUI")
298
+
299
+ parser.add_argument("--dev-mode", action="store_true",
300
+ help="If set, will expose development elements")
301
+
302
+ parser.add_argument("--log-mode", type=str, default=const.LOG_FULL,
303
+ help="How logs should be stored. Allowed values are: \n"
304
+ "- 'full' - Multi-file logging, not recommended for the CLI, but allowed for a compatible "
305
+ "interface with the public web app"
306
+ "- 'simple' - Logs saved to one file"
307
+ "- 'stdout' - Output logs and errors only to stdout"
308
+ "- 'none' - Output only errors to stdout")
309
+
310
+ parser.add_argument("--log-level", type=str, default=None,
311
+ help="The desired level to log at. Allowed values are: 'DEBUG', 'INFO', 'WARNING', 'ERROR, "
312
+ "'CRITICAL'. Default: 'INFO' for logging to file, 'WARNING' for logging to stdout")
313
+
314
+ # Set global variables for settings based on parsed arguments, unless it's set to use env vars
315
+ args = parser.parse_args()
316
+
317
+ if not args.use_env_vars:
318
+
319
+ global max_file_size
320
+ max_file_size = args.max_file_size*const.MEGABYTE
321
+
322
+ global max_file_size_ob
323
+ max_file_size_ob = args.max_file_size_ob*const.MEGABYTE
324
+
325
+ global service_mode
326
+ service_mode = args.service_mode
327
+
328
+ global production_mode
329
+ production_mode = not args.dev_mode
330
+
331
+ global log_mode
332
+ log_mode = args.log_mode
333
+
334
+ global log_level
335
+ log_level = args.log_level
336
+
337
+ start_app()
338
+
339
+
340
+ if __name__ == "__main__":
341
+
342
+ main()
@@ -40,6 +40,7 @@ CL_SCRIPT_NAME = "psdi-data-convert"
40
40
  LOG_MODE_EV = "LOG_MODE"
41
41
  LOG_LEVEL_EV = "LOG_LEVEL"
42
42
  MAX_FILESIZE_EV = "MAX_FILESIZE"
43
+ MAX_FILESIZE_OB_EV = "MAX_FILESIZE_OB"
43
44
 
44
45
  # Files and Folders
45
46
  # -----------------
@@ -47,6 +48,7 @@ MAX_FILESIZE_EV = "MAX_FILESIZE"
47
48
  # Maximum output file size in bytes
48
49
  MEGABYTE = 1024*1024
49
50
  DEFAULT_MAX_FILE_SIZE = 0*MEGABYTE
51
+ DEFAULT_MAX_FILE_SIZE_OB = 1*MEGABYTE
50
52
 
51
53
  DEFAULT_UPLOAD_DIR = './psdi_data_conversion/static/uploads'
52
54
  DEFAULT_DOWNLOAD_DIR = './psdi_data_conversion/static/downloads'
@@ -17,6 +17,7 @@ from psdi_data_conversion.converters import base
17
17
 
18
18
  import glob
19
19
 
20
+ from psdi_data_conversion.converters.openbabel import CONVERTER_OB
20
21
  from psdi_data_conversion.file_io import (is_archive, is_supported_archive, pack_zip_or_tar, split_archive_ext,
21
22
  unpack_zip_or_tar)
22
23
 
@@ -105,7 +106,8 @@ def get_converter(*args, name=const.CONVERTER_DEFAULT, **converter_kwargs) -> ba
105
106
  name : str
106
107
  The desired converter type, by default 'Open Babel'
107
108
  data : dict[str | Any] | None
108
- A dict of any other data needed by a converter or for extra logging information, default empty dict
109
+ A dict of any other data needed by a converter or for extra logging information, default empty dict. See the
110
+ docstring of each converter for supported keys and values that can be passed to `data` here
109
111
  abort_callback : Callable[[int], None]
110
112
  Function to be called if the conversion hits an error and must be aborted, default `abort_raise`, which
111
113
  raises an appropriate exception
@@ -117,7 +119,9 @@ def get_converter(*args, name=const.CONVERTER_DEFAULT, **converter_kwargs) -> ba
117
119
  download_dir : str
118
120
  The location of output files relative to the current directory
119
121
  max_file_size : float
120
- The maximum allowed file size for input/output files, in MB, default 1 MB. If 0, will be unlimited
122
+ The maximum allowed file size for input/output files, in MB, default 1 MB for Open Babel, unlimited for other
123
+ converters. If 0, will be unlimited. If an archive of files is provided, this will apply to the total of all
124
+ files contained in it
121
125
  no_check : bool
122
126
  If False (default), will check at setup whether or not a conversion between the desired file formats is
123
127
  supported with the specified converter
@@ -238,7 +242,7 @@ def run_converter(filename: str,
238
242
  *args,
239
243
  from_format: str | None = None,
240
244
  download_dir=const.DEFAULT_DOWNLOAD_DIR,
241
- max_file_size=const.DEFAULT_MAX_FILE_SIZE,
245
+ max_file_size=None,
242
246
  log_file: str | None = None,
243
247
  log_mode=const.LOG_SIMPLE,
244
248
  strict=False,
@@ -261,7 +265,8 @@ def run_converter(filename: str,
261
265
  name : str
262
266
  The desired converter type, by default 'Open Babel'
263
267
  data : dict[str | Any] | None
264
- A dict of any other data needed by a converter or for extra logging information, default empty dict
268
+ A dict of any other data needed by a converter or for extra logging information, default empty dict. See the
269
+ docstring of each converter for supported keys and values that can be passed to `data` here
265
270
  abort_callback : Callable[[int], None]
266
271
  Function to be called if the conversion hits an error and must be aborted, default `abort_raise`, which
267
272
  raises an appropriate exception
@@ -280,8 +285,9 @@ def run_converter(filename: str,
280
285
  into a file of the same format, their logs will be combined into a single log, and the converted files and
281
286
  individual logs will be deleted
282
287
  max_file_size : float
283
- The maximum allowed file size for input/output files, in MB, default 1 MB. If 0, will be unlimited. If an
284
- archive of files is provided, this will apply to the total of all files contained in it
288
+ The maximum allowed file size for input/output files, in MB, default 1 MB for Open Babel, unlimited for other
289
+ converters. If 0, will be unlimited. If an archive of files is provided, this will apply to the total of all
290
+ files contained in it
285
291
  no_check : bool
286
292
  If False (default), will check at setup whether or not a conversion between the desired file formats is
287
293
  supported with the specified converter
@@ -318,6 +324,10 @@ def run_converter(filename: str,
318
324
  If something goes wrong during the conversion process
319
325
  """
320
326
 
327
+ # Set the maximum file size based on which converter is being used if using the default
328
+ if max_file_size is None:
329
+ max_file_size = const.DEFAULT_MAX_FILE_SIZE_OB if name == CONVERTER_OB else const.DEFAULT_MAX_FILE_SIZE
330
+
321
331
  # Set the log file if it was unset - note that in server logging mode, this value won't be used within the
322
332
  # converter class, so it needs to be set up here to match what will be set up there
323
333
  if log_file is None:
@@ -11,7 +11,9 @@ CONVERTER_ATO = 'Atomsk'
11
11
 
12
12
 
13
13
  class AtoFileConverter(ScriptFileConverter):
14
- """File Converter specialized to use Atomsk for conversions
14
+ """File Converter specialized to use Atomsk for conversions.
15
+
16
+ This converter does not yet support any additional configuration options provided at class init to the `data` kwarg.
15
17
  """
16
18
 
17
19
  name = CONVERTER_ATO
@@ -34,7 +34,12 @@ except ImportError:
34
34
  class FileConverterException(RuntimeError):
35
35
  """Exception class to represent any runtime error encountered by this package.
36
36
  """
37
- pass
37
+
38
+ def __init__(self,
39
+ *args,
40
+ logged: bool = False):
41
+ super().__init__(*args)
42
+ self.logged = logged
38
43
 
39
44
 
40
45
  class FileConverterAbortException(FileConverterException):
@@ -212,7 +217,7 @@ class FileConverter:
212
217
  use_envvars=False,
213
218
  upload_dir=const.DEFAULT_UPLOAD_DIR,
214
219
  download_dir=const.DEFAULT_DOWNLOAD_DIR,
215
- max_file_size=const.DEFAULT_MAX_FILE_SIZE,
220
+ max_file_size=None,
216
221
  no_check=False,
217
222
  log_file: str | None = None,
218
223
  log_mode=const.LOG_FULL,
@@ -231,7 +236,8 @@ class FileConverter:
231
236
  The format to convert from, as the file extension (e.g. "pdb"). If None is provided (default), will be
232
237
  determined from the extension of `filename`
233
238
  data : dict[str | Any] | None
234
- A dict of any other data needed by a converter or for extra logging information, default empty dict
239
+ A dict of any other data needed by a converter or for extra logging information, default empty dict. See the
240
+ docstring of each converter for supported keys and values that can be passed to `data` here
235
241
  abort_callback : Callable[[int], None]
236
242
  Function to be called if the conversion hits an error and must be aborted, default `abort_raise`, which
237
243
  raises an appropriate exception
@@ -275,12 +281,32 @@ class FileConverter:
275
281
 
276
282
  try:
277
283
 
284
+ if max_file_size is None:
285
+ from psdi_data_conversion.converters.openbabel import CONVERTER_OB
286
+ if self.name == CONVERTER_OB:
287
+ self.max_file_size = const.DEFAULT_MAX_FILE_SIZE_OB
288
+ else:
289
+ self.max_file_size = const.DEFAULT_MAX_FILE_SIZE
290
+ else:
291
+ self.max_file_size = max_file_size*const.MEGABYTE
292
+
293
+ # Set values from envvars if desired
294
+ if use_envvars:
295
+ # Get the maximum allowed size from the envvar for it
296
+ from psdi_data_conversion.converters.openbabel import CONVERTER_OB
297
+ if self.name == CONVERTER_OB:
298
+ ev_max_file_size = os.environ.get(const.MAX_FILESIZE_OB_EV)
299
+ else:
300
+ ev_max_file_size = os.environ.get(const.MAX_FILESIZE_EV)
301
+
302
+ if ev_max_file_size is not None:
303
+ self.max_file_size = float(ev_max_file_size)*const.MEGABYTE
304
+
278
305
  # Set member variables directly from input
279
306
  self.in_filename = filename
280
307
  self.to_format = to_format
281
308
  self.upload_dir = upload_dir
282
309
  self.download_dir = download_dir
283
- self.max_file_size = max_file_size*const.MEGABYTE
284
310
  self.log_file = log_file
285
311
  self.log_mode = log_mode
286
312
  self.log_level = log_level
@@ -312,13 +338,6 @@ class FileConverter:
312
338
  self.err: str | None = None
313
339
  self.quality: str | None = None
314
340
 
315
- # Set values from envvars if desired
316
- if use_envvars:
317
- # Get the maximum allowed size from the envvar for it
318
- ev_max_file_size = os.environ.get(const.MAX_FILESIZE_EV)
319
- if ev_max_file_size is not None:
320
- self.max_file_size = float(ev_max_file_size)*const.MEGABYTE
321
-
322
341
  # Create directory 'uploads' if not extant.
323
342
  if not os.path.exists(self.upload_dir):
324
343
  os.makedirs(self.upload_dir, exist_ok=True)
@@ -351,10 +370,13 @@ class FileConverter:
351
370
  self.logger.debug("Finished FileConverter initialisation")
352
371
 
353
372
  except Exception as e:
373
+ # Don't catch a deliberate abort; let it pass through
354
374
  if isinstance(e, l_abort_exceptions):
355
- # Don't catch a deliberate abort; let it pass through
356
- self.logger.error(f"Unexpected exception raised while initializing the converter, of type '{type(e)}' "
357
- f"with message: {str(e)}")
375
+ if not hasattr(e, "logged") or e.logged is False:
376
+ self.logger.error(f"Unexpected exception raised while running the converter, of type '{type(e)}' "
377
+ f"with message: {str(e)}")
378
+ if e:
379
+ e.logged = True
358
380
  raise
359
381
  # Try to run the standard abort method. There's a good chance this will fail though depending on what went
360
382
  # wrong when during init, so we fallback to printing the exception to stderr
@@ -362,6 +384,8 @@ class FileConverter:
362
384
  if not isinstance(e, FileConverterHelpException):
363
385
  self.logger.error(f"Exception triggering an abort was raised while initializing the converter. "
364
386
  f"Exception was type '{type(e)}', with message: {str(e)}")
387
+ if e:
388
+ e.logged = True
365
389
  self._abort(message="The application encountered an error while initializing the converter:\n" +
366
390
  traceback.format_exc(), e=e)
367
391
  except Exception as ee:
@@ -445,20 +469,28 @@ class FileConverter:
445
469
  """
446
470
 
447
471
  try:
472
+ self.logger.debug("Checking input file size")
473
+ self._check_input_file_size_and_status()
474
+
448
475
  self.logger.debug("Starting file conversion")
449
476
  self._convert()
450
477
 
451
478
  self.logger.debug("Finished file conversion; performing cleanup tasks")
452
479
  self._finish_convert()
453
480
  except Exception as e:
481
+ # Don't catch a deliberate abort; let it pass through
454
482
  if isinstance(e, l_abort_exceptions):
455
- # Don't catch a deliberate abort; let it pass through
456
- self.logger.error(f"Unexpected exception raised while running the converter, of type '{type(e)}' with "
457
- f"message: {str(e)}")
483
+ # Log the error if it hasn't yet been logged
484
+ if not hasattr(e, "logged") or e.logged is False:
485
+ self.logger.error(f"Unexpected exception raised while running the converter, of type '{type(e)}' "
486
+ f"with message: {str(e)}")
487
+ e.logged = True
458
488
  raise
459
489
  if not isinstance(e, FileConverterHelpException):
460
490
  self.logger.error(f"Exception triggering an abort was raised while running the converter. Exception "
461
491
  f"was type '{type(e)}', with message: {str(e)}")
492
+ if e:
493
+ e.logged = True
462
494
  self._abort(message="The application encountered an error while running the converter:\n" +
463
495
  traceback.format_exc(), e=e)
464
496
 
@@ -522,13 +554,23 @@ class FileConverter:
522
554
  # Note this message in the dev logger as well
523
555
  if not isinstance(e, FileConverterHelpException):
524
556
  self.logger.error(message)
557
+ if e:
558
+ e.logged = True
525
559
 
526
- # Call the abort callback function now. We first try to add information to it, but in case that isn't supported,
527
- # we fall back to just calling it with the status code
560
+ # Call the abort callback function now. We first try passing information to the callback function
528
561
  try:
529
562
  self.abort_callback(status_code, message, e=e, **kwargs)
530
563
  except TypeError:
531
- self.abort_callback(status_code)
564
+ # The callback function doesn't support arguments, so we instead call the callback, catch any exception it
565
+ # raises, monkey-patch on the extra info, and reraise it
566
+ try:
567
+ self.abort_callback(status_code)
568
+ except Exception as ee:
569
+ ee.status_code = status_code
570
+ ee.message = message
571
+ ee.e = e
572
+ ee.abort_kwargs = kwargs
573
+ raise ee
532
574
 
533
575
  def _abort_from_err(self):
534
576
  """Call an abort after a call to the converter has completed, but it's returned an error. Create a message for
@@ -538,7 +580,7 @@ class FileConverter:
538
580
  self._create_message() +
539
581
  self.out + '\n' +
540
582
  self.err)
541
- self._abort(message=self.err)
583
+ self._abort(message=self.err, logged=True)
542
584
 
543
585
  def _create_message(self) -> str:
544
586
  """Create a log of options passed to the converter - this method should be overloaded to log any information
@@ -578,19 +620,38 @@ class FileConverter:
578
620
 
579
621
  self.logger.info(message)
580
622
 
581
- def _check_file_size_and_status(self):
582
- """Get file sizes, checking that output file isn't too large
623
+ def _check_input_file_size_and_status(self):
624
+ """Get input file size and status, checking that the file isn't too large
625
+ """
583
626
 
584
- Returns
585
- -------
586
- in_size : int
587
- Size of input file in bytes
588
- out_size : int
589
- Size of output file in bytes
627
+ try:
628
+ self.in_size = os.path.getsize(os.path.realpath(self.in_filename))
629
+ except FileNotFoundError:
630
+ # Something went wrong and the output file doesn't exist
631
+ err_message = f"Expected output file {self.in_filename} does not exist."
632
+ self.logger.error(err_message)
633
+ self.err += f"ERROR: {err_message}\n"
634
+ self._abort_from_err()
635
+
636
+ # Check that the input file doesn't exceed the maximum allowed size
637
+ if self.max_file_size > 0 and self.in_size > self.max_file_size:
638
+
639
+ self._abort(const.STATUS_CODE_SIZE,
640
+ f"ERROR converting {os.path.basename(self.in_filename)} to " +
641
+ os.path.basename(self.out_filename) + ": "
642
+ f"Input file exceeds maximum size.\nInput file size is "
643
+ f"{self.in_size/const.MEGABYTE:.2f} MB; maximum input file size is "
644
+ f"{self.max_file_size/const.MEGABYTE:.2f} MB.\n",
645
+ max_file_size=self.max_file_size,
646
+ in_size=self.in_size,
647
+ out_size=None)
648
+
649
+ def _check_output_file_size_and_status(self):
650
+ """Get output file size and status, checking that the file isn't too large
590
651
  """
591
- in_size = os.path.getsize(os.path.realpath(self.in_filename))
652
+
592
653
  try:
593
- out_size = os.path.getsize(os.path.realpath(self.out_filename))
654
+ self.out_size = os.path.getsize(os.path.realpath(self.out_filename))
594
655
  except FileNotFoundError:
595
656
  # Something went wrong and the output file doesn't exist
596
657
  err_message = f"Expected output file {self.out_filename} does not exist."
@@ -599,20 +660,19 @@ class FileConverter:
599
660
  self._abort_from_err()
600
661
 
601
662
  # Check that the output file doesn't exceed the maximum allowed size
602
- if self.max_file_size > 0 and out_size > self.max_file_size:
663
+ if self.max_file_size > 0 and self.out_size > self.max_file_size:
603
664
 
604
665
  self._abort(const.STATUS_CODE_SIZE,
605
666
  f"ERROR converting {os.path.basename(self.in_filename)} to " +
606
667
  os.path.basename(self.out_filename) + ": "
607
668
  f"Output file exceeds maximum size.\nInput file size is "
608
- f"{in_size/const.MEGABYTE:.2f} MB; Output file size is {out_size/const.MEGABYTE:.2f} "
669
+ f"{self.in_size/const.MEGABYTE:.2f} MB; Output file size is {self.out_size/const.MEGABYTE:.2f} "
609
670
  f"MB; maximum output file size is {self.max_file_size/const.MEGABYTE:.2f} MB.\n",
610
- in_size=in_size,
611
- out_size=out_size,
612
- max_file_size=self.max_file_size)
613
- self.logger.debug(f"Output file found to have size {out_size/const.MEGABYTE:.2f} MB")
671
+ max_file_size=self.max_file_size,
672
+ in_size=self.in_size,
673
+ out_size=self.out_size)
614
674
 
615
- return in_size, out_size
675
+ self.logger.debug(f"Output file found to have size {self.out_size/const.MEGABYTE:.2f} MB")
616
676
 
617
677
  def get_quality(self) -> str:
618
678
  """Query the JSON file to obtain conversion quality
@@ -630,7 +690,7 @@ class FileConverter:
630
690
  """Run final common steps to clean up a conversion and log success or abort due to an error
631
691
  """
632
692
 
633
- self.in_size, self.out_size = self._check_file_size_and_status()
693
+ self._check_output_file_size_and_status()
634
694
 
635
695
  if self.delete_input:
636
696
  os.remove(self.in_filename)
@@ -11,7 +11,9 @@ CONVERTER_C2X = 'c2x'
11
11
 
12
12
 
13
13
  class C2xFileConverter(ScriptFileConverter):
14
- """File Converter specialized to use c2x for conversions
14
+ """File Converter specialized to use c2x for conversions.
15
+
16
+ This converter does not yet support any additional configuration options provided at class init to the `data` kwarg.
15
17
  """
16
18
 
17
19
  name = CONVERTER_C2X
@@ -76,7 +76,46 @@ def get_coord_gen(l_opts: list[str] | None) -> dict[str, str]:
76
76
 
77
77
 
78
78
  class OBFileConverter(FileConverter):
79
- """File Converter specialized to use Open Babel for conversions
79
+ """File Converter specialized to use Open Babel for conversions.
80
+
81
+ This converter supports some additional configuration options which can be provided at class init or call to
82
+ `run_converter()` through providing a dict to the `data` kwarg. The supported keys and values are:
83
+
84
+ "from_flags": str
85
+ String of concatenated one-letter flags for how to read the input file. To list the flags supported for a given
86
+ input format, call ``psdi-data-convert -l -f <format> -w Open Babel`` at the command-line and look for the
87
+ "Allowed input flags" section, if one exists, or alternatively call the library function
88
+ ``psdi_data_conversion.database.get_in_format_args("Open Babel", <format>)`` from within Python code.
89
+
90
+ "to_flags": str
91
+ String of concatenated one-letter flags for how to write the output file. To list the flags supported for a
92
+ given output format, call ``psdi-data-convert -l -f <format> -w Open Babel`` at the command-line and look for
93
+ the "Allowed output flags" section, if one exists, or alternatively call the library function
94
+ ``psdi_data_conversion.database.get_out_format_args("Open Babel", <format>)`` from within Python code.
95
+
96
+ "from_options": str
97
+ String of space-separated options for how to read the input file. Each option "word" in this string should start
98
+ with the letter indicating which option is being used, followed by the value for that option. To list the
99
+ options supported for a given input format, call ``psdi-data-convert -l -f <format> -w Open Babel`` at the
100
+ command-line and look for the "Allowed input options" section, if one exists, or alternatively call the library
101
+ function ``psdi_data_conversion.database.get_in_format_args("Open Babel", <format>)`` from within Python code.
102
+
103
+ "to_options": str
104
+ String of space-separated options for how to write the output file. Each option "word" in this string should
105
+ start with the letter indicating which option is being used, followed by the value for that option. To list the
106
+ options supported for a given output format, call ``psdi-data-convert -l -t <format> -w Open Babel`` at the
107
+ command-line and look for the "Allowed output options" section, if one exists, or alternatively call the library
108
+ function ``psdi_data_conversion.database.get_out_format_args("Open Babel", <format>)`` from within Python code.
109
+
110
+ "coordinates": str
111
+ One of "Gen2D", "Gen3D", or "neither", specifying how positional coordinates should be generated in the output
112
+ file. Default "neither"
113
+
114
+ "coordOption": str
115
+ One of "fastest", "fast", "medium", "better", or "best", specifying the quality of the calculation of
116
+ coordinates. Default "medium"
117
+
118
+ Note that some other keys are supported for compatibility purposes, but these may be deprecated in the future.
80
119
  """
81
120
 
82
121
  name = CONVERTER_OB
@@ -284,6 +284,11 @@ class ConverterInfo:
284
284
  d_format_args: dict[str | int, set[ArgInfo]] = {}
285
285
  l_parent_format_info = self.parent.l_format_info
286
286
 
287
+ # If the converter doesn't provide argument info, set l_arg_info to an empty list so it can be iterated in
288
+ # the next step, rather than None
289
+ if not l_arg_info:
290
+ l_arg_info = []
291
+
287
292
  for arg_info in l_arg_info:
288
293
 
289
294
  if arg_info is None: