psdi-data-conversion 0.0.39__py3-none-any.whl → 0.1.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.
@@ -13,12 +13,14 @@ from collections.abc import Callable
13
13
  from datetime import datetime
14
14
  from functools import wraps
15
15
  from hashlib import md5
16
+ from multiprocessing import Lock
16
17
  from subprocess import run
17
18
  from traceback import format_exc
18
19
  from typing import Any
19
20
 
20
21
  import werkzeug.serving
21
22
  from flask import Flask, Response, abort, cli, render_template, request
23
+ from werkzeug.utils import secure_filename
22
24
 
23
25
  import psdi_data_conversion
24
26
  from psdi_data_conversion import constants as const
@@ -31,24 +33,29 @@ from psdi_data_conversion.main import print_wrap
31
33
  # Env var for the SHA of the latest commit
32
34
  SHA_EV = "SHA"
33
35
 
34
- # Env var for whether this is running in service mode or locally
35
- SERVICE_MODE_EV = "SERVICE_MODE"
36
-
37
36
  # Env var for whether this is a production release or development
38
37
  PRODUCTION_EV = "PRODUCTION_MODE"
39
38
 
39
+ # Env var for whether this is a production release or development
40
+ DEBUG_EV = "DEBUG_MODE"
41
+
40
42
  # Key for the label given to the file uploaded in the web interface
41
43
  FILE_TO_UPLOAD_KEY = 'fileToUpload'
42
44
 
45
+ # A lock to prevent multiple threads from logging at the same time
46
+ logLock = Lock()
47
+
43
48
  # Create a token by hashing the current date and time.
44
49
  dt = str(datetime.now())
45
50
  token = md5(dt.encode('utf8')).hexdigest()
46
51
 
47
- # Get the service and production modes from their envvars
48
- service_mode_ev = os.environ.get(SERVICE_MODE_EV)
52
+ # Get the debug, service and production modes from their envvars
53
+ service_mode_ev = os.environ.get(const.SERVICE_MODE_EV)
49
54
  service_mode = (service_mode_ev is not None) and (service_mode_ev.lower() == "true")
50
55
  production_mode_ev = os.environ.get(PRODUCTION_EV)
51
56
  production_mode = (production_mode_ev is not None) and (production_mode_ev.lower() == "true")
57
+ debug_mode_ev = os.environ.get(DEBUG_EV)
58
+ debug_mode = (debug_mode_ev is not None) and (debug_mode_ev.lower() == "true")
52
59
 
53
60
  # Get the logging mode and level from their envvars
54
61
  ev_log_mode = os.environ.get(const.LOG_MODE_EV)
@@ -86,11 +93,11 @@ if ev_max_file_size_ob is not None:
86
93
  else:
87
94
  max_file_size_ob = const.DEFAULT_MAX_FILE_SIZE_OB
88
95
 
89
- # Since we're using the development server as the user GUI, we monkey-patch Flask to disable the warnings that would
90
- # otherwise appear for this so they don't confuse the user
91
-
92
96
 
93
97
  def suppress_warning(func: Callable[..., Any]) -> Callable[..., Any]:
98
+ """Since we're using the development server as the user GUI, we monkey-patch Flask to disable the warnings that
99
+ would otherwise appear for this so they don't confuse the user
100
+ """
94
101
  @wraps(func)
95
102
  def wrapper(*args, **kwargs) -> Any:
96
103
  if args and isinstance(args[0], str) and args[0].startswith('WARNING: This is a development server.'):
@@ -105,43 +112,86 @@ cli.show_server_banner = lambda *_: None
105
112
  app = Flask(__name__)
106
113
 
107
114
 
108
- def get_last_sha() -> str:
115
+ def limit_upload_size():
116
+ """Impose a limit on the maximum file that can be uploaded before Flask will raise an error"""
117
+
118
+ # Determine the largest possible file size that can be uploaded, keeping in mind that 0 indicates unlimited
119
+ larger_max_file_size = max_file_size
120
+ if (max_file_size > 0) and (max_file_size_ob > max_file_size):
121
+ larger_max_file_size = max_file_size_ob
122
+
123
+ if larger_max_file_size > 0:
124
+ app.config['MAX_CONTENT_LENGTH'] = larger_max_file_size
125
+ else:
126
+ app.config['MAX_CONTENT_LENGTH'] = None
127
+
128
+
129
+ # Set the upload limit based on env vars to start with
130
+ limit_upload_size()
131
+
132
+
133
+ def get_tag_and_sha() -> str:
109
134
  """Get the SHA of the last commit
110
135
  """
111
136
 
112
- # First check if the SHA is provided through an environmental variable
113
- ev_sha = os.environ.get(SHA_EV)
114
- if ev_sha:
115
- return ev_sha
116
-
117
137
  try:
118
- # This bash command calls `git log` to get info on the last commit, uses `head` to trim it to one line, then
119
- # uses `gawk` to get just the second word of this line, which is the SHA of this commit
120
- cmd = "git log -n 1 | head -n 1 | gawk '{print($2)}'"
138
+ # This bash command calls `git tag` to get a sorted list of tags, with the most recent at the top, then uses
139
+ # `head` to trim it to one line
140
+ cmd = "git tag --sort -version:refname | head -n 1"
121
141
 
122
142
  out_bytes = run(cmd, shell=True, capture_output=True).stdout
123
- out_str = str(out_bytes.decode()).strip()
143
+ tag = str(out_bytes.decode()).strip()
144
+
145
+ # Get the SHA associated with this tag
146
+ cmd = f"git show {tag}" + " | head -n 1 | gawk '{print($2)}'"
147
+
148
+ out_bytes = run(cmd, shell=True, capture_output=True).stdout
149
+ tag_sha = str(out_bytes.decode()).strip()
124
150
 
125
151
  except Exception:
126
- print("ERROR: Could not determine SHA of most recent commit. Error was:\n" + format_exc(),
152
+ print("ERROR: Could not determine most recent tag. Error was:\n" + format_exc(),
127
153
  file=sys.stderr)
128
- out_str = "N/A"
154
+ tag = ""
155
+ tag_sha = None
129
156
 
130
- return out_str
157
+ # First check if the SHA is provided through an environmental variable
158
+ ev_sha = os.environ.get(SHA_EV)
159
+ if ev_sha:
160
+ sha = ev_sha
161
+ else:
162
+ try:
163
+ # This bash command calls `git log` to get info on the last commit, uses `head` to trim it to one line, then
164
+ # uses `gawk` to get just the second word of this line, which is the SHA of this commit
165
+ cmd = "git log -n 1 | head -n 1 | gawk '{print($2)}'"
166
+
167
+ out_bytes = run(cmd, shell=True, capture_output=True).stdout
168
+ sha = str(out_bytes.decode()).strip()
169
+
170
+ except Exception:
171
+ print("ERROR: Could not determine SHA of most recent commit. Error was:\n" + format_exc(),
172
+ file=sys.stderr)
173
+ sha = ""
174
+
175
+ # If the SHA of the tag is the same as the current SHA, we indicate this by returning a blank SHA
176
+ if tag_sha == sha:
177
+ sha = ""
178
+
179
+ return (tag, sha)
131
180
 
132
181
 
133
182
  @app.route('/')
134
183
  def website():
135
184
  """Return the web page along with the token
136
185
  """
137
-
138
- data = [{'token': token,
139
- 'max_file_size': max_file_size,
140
- 'max_file_size_ob': max_file_size_ob,
141
- 'service_mode': service_mode,
142
- 'production_mode': production_mode,
143
- 'sha': get_last_sha()}]
144
- return render_template("index.htm", data=data)
186
+ tag, sha = get_tag_and_sha()
187
+ return render_template("index.htm",
188
+ token=token,
189
+ max_file_size=max_file_size,
190
+ max_file_size_ob=max_file_size_ob,
191
+ service_mode=service_mode,
192
+ production_mode=production_mode,
193
+ tag=tag,
194
+ sha=sha)
145
195
 
146
196
 
147
197
  @app.route('/convert/', methods=['POST'])
@@ -153,9 +203,8 @@ def convert():
153
203
  # Make sure the upload directory exists
154
204
  os.makedirs(const.DEFAULT_UPLOAD_DIR, exist_ok=True)
155
205
 
156
- # Save the file in the upload directory
157
206
  file = request.files[FILE_TO_UPLOAD_KEY]
158
- filename = filename = file.filename
207
+ filename = secure_filename(file.filename)
159
208
 
160
209
  qualified_filename = os.path.join(const.DEFAULT_UPLOAD_DIR, filename)
161
210
  file.save(qualified_filename)
@@ -248,7 +297,10 @@ def feedback():
248
297
  if key in report:
249
298
  entry[key] = str(report[key])
250
299
 
251
- log_utility.append_to_log_file("feedback", entry)
300
+ # Write data in JSON format and send to stdout
301
+ logLock.acquire()
302
+ sys.stdout.write(f"{json.dumps(entry) + '\n'}")
303
+ logLock.release()
252
304
 
253
305
  return Response(status=201)
254
306
 
@@ -318,7 +370,7 @@ def start_app():
318
370
  """
319
371
 
320
372
  os.chdir(os.path.join(psdi_data_conversion.__path__[0], ".."))
321
- app.run()
373
+ app.run(debug=debug_mode)
322
374
 
323
375
 
324
376
  def main():
@@ -343,7 +395,11 @@ def main():
343
395
  help="If set, will run as if deploying a service rather than the local GUI")
344
396
 
345
397
  parser.add_argument("--dev-mode", action="store_true",
346
- help="If set, will expose development elements")
398
+ help="If set, will expose development elements, such as the SHA of the latest commit")
399
+
400
+ parser.add_argument("--debug", action="store_true",
401
+ help="If set, will run the Flask server in debug mode, which will cause it to automatically "
402
+ "reload if code changes and show an interactive debugger in the case of errors")
347
403
 
348
404
  parser.add_argument("--log-mode", type=str, default=const.LOG_FULL,
349
405
  help="How logs should be stored. Allowed values are: \n"
@@ -371,6 +427,9 @@ def main():
371
427
  global service_mode
372
428
  service_mode = args.service_mode
373
429
 
430
+ global debug_mode
431
+ debug_mode = args.debug
432
+
374
433
  global production_mode
375
434
  production_mode = not args.dev_mode
376
435
 
@@ -380,6 +439,9 @@ def main():
380
439
  global log_level
381
440
  log_level = args.log_level
382
441
 
442
+ # Set the upload limit based on provided arguments now
443
+ limit_upload_size()
444
+
383
445
  print_wrap("Starting the PSDI Data Conversion GUI. This GUI is run as a webpage, which you can open by "
384
446
  "right-clicking the link below to open it in your default browser, or by copy-and-pasting it into your "
385
447
  "browser of choice.")
@@ -41,6 +41,7 @@ LOG_MODE_EV = "LOG_MODE"
41
41
  LOG_LEVEL_EV = "LOG_LEVEL"
42
42
  MAX_FILESIZE_EV = "MAX_FILESIZE"
43
43
  MAX_FILESIZE_OB_EV = "MAX_FILESIZE_OB"
44
+ SERVICE_MODE_EV = "SERVICE_MODE"
44
45
 
45
46
  # Files and Folders
46
47
  # -----------------
@@ -5,11 +5,16 @@ Created 2024-12-10 by Bryan Gillis.
5
5
  Class and functions to perform file conversion
6
6
  """
7
7
 
8
+ from dataclasses import dataclass, field
9
+ import json
8
10
  import glob
9
11
  import importlib
10
12
  import os
11
13
  import sys
12
14
  import traceback
15
+ from typing import Any, Callable, NamedTuple
16
+ from multiprocessing import Lock
17
+ from psdi_data_conversion import log_utility
13
18
  from collections.abc import Callable
14
19
  from dataclasses import dataclass, field
15
20
  from tempfile import TemporaryDirectory
@@ -20,6 +25,10 @@ from psdi_data_conversion.converters import base
20
25
  from psdi_data_conversion.converters.openbabel import CONVERTER_OB
21
26
  from psdi_data_conversion.file_io import (is_archive, is_supported_archive, pack_zip_or_tar, split_archive_ext,
22
27
  unpack_zip_or_tar)
28
+ from psdi_data_conversion.utils import regularize_name
29
+
30
+ # A lock to prevent multiple threads from logging at the same time
31
+ logLock = Lock()
23
32
 
24
33
  # Find all modules for specific converters
25
34
  l_converter_modules = glob.glob(os.path.dirname(base.__file__) + "/*.py")
@@ -49,7 +58,8 @@ try:
49
58
 
50
59
  converter_class = module.converter
51
60
 
52
- name = converter_class.name
61
+ # To make querying case/space-insensitive, we store all names in lowercase with spaces stripped
62
+ name = converter_class.name.lower().replace(" ", "")
53
63
 
54
64
  return NameAndClass(name, converter_class)
55
65
 
@@ -91,6 +101,66 @@ except Exception:
91
101
  D_CONVERTER_ARGS = {}
92
102
 
93
103
 
104
+ def get_supported_converter_class(name: str):
105
+ """Get the appropriate converter class matching the provided name from the dict of supported converters
106
+
107
+ Parameters
108
+ ----------
109
+ name : str
110
+ Converter name (case- and space-insensitive)
111
+
112
+ Returns
113
+ -------
114
+ type[base.FileConverter]
115
+ """
116
+ return D_SUPPORTED_CONVERTERS[regularize_name(name)]
117
+
118
+
119
+ def get_registered_converter_class(name: str):
120
+ """Get the appropriate converter class matching the provided name from the dict of supported converters
121
+
122
+ Parameters
123
+ ----------
124
+ name : str
125
+ Converter name (case- and space-insensitive)
126
+
127
+ Returns
128
+ -------
129
+ type[base.FileConverter]
130
+ """
131
+ return D_REGISTERED_CONVERTERS[regularize_name(name)]
132
+
133
+
134
+ def converter_is_supported(name: str):
135
+ """Checks if a converter is supported in principle by this project
136
+
137
+ Parameters
138
+ ----------
139
+ name : str
140
+ Converter name (case- and space-insensitive)
141
+
142
+ Returns
143
+ -------
144
+ bool
145
+ """
146
+ return regularize_name(name) in L_SUPPORTED_CONVERTERS
147
+
148
+
149
+ def converter_is_registered(name: str):
150
+ """Checks if a converter is registered (usable)
151
+
152
+ Parameters
153
+ ----------
154
+ name : str
155
+ Converter name (case- and space-insensitive)
156
+
157
+ Returns
158
+ -------
159
+ bool
160
+ """
161
+ return regularize_name(name) in L_REGISTERED_CONVERTERS
162
+
163
+
94
164
  def get_converter(*args, name=const.CONVERTER_DEFAULT, **converter_kwargs) -> base.FileConverter:
95
165
  """Get a FileConverter of the proper subclass for the requested converter type
96
166
 
@@ -129,7 +199,7 @@ def get_converter(*args, name=const.CONVERTER_DEFAULT, **converter_kwargs) -> ba
129
199
  If provided, all logging will go to a single file or stream. Otherwise, logs will be split up among multiple
130
200
  files for server-style logging.
131
201
  log_mode : str
132
- How logs should be stores. Allowed values are:
202
+ How logs should be stored. Allowed values are:
133
203
  - 'full' - Multi-file logging, only recommended when running as a public web app
134
204
  - 'simple' - Logs saved to one file
135
205
  - 'stdout' - Output logs and errors only to stdout
@@ -155,10 +225,11 @@ def get_converter(*args, name=const.CONVERTER_DEFAULT, **converter_kwargs) -> ba
155
225
  FileConverterInputException
156
226
  If the converter isn't recognized or there's some other issue with the input
157
227
  """
228
+ name = regularize_name(name)
158
229
  if name not in L_REGISTERED_CONVERTERS:
159
230
  raise base.FileConverterInputException(const.ERR_CONVERTER_NOT_RECOGNISED.format(name) +
160
231
  f"{L_REGISTERED_CONVERTERS}")
161
- converter_class = D_REGISTERED_CONVERTERS[name]
232
+ converter_class = get_registered_converter_class(name)
162
233
 
163
234
  return converter_class(*args, **converter_kwargs)
164
235
 
@@ -302,7 +373,7 @@ def run_converter(filename: str,
302
373
  If provided, all logging will go to a single file or stream. Otherwise, logs will be split up among multiple
303
374
  files for server-style logging.
304
375
  log_mode : str
305
- How logs should be stores. Allowed values are:
376
+ How logs should be stored. Allowed values are:
306
377
  - 'full' - Multi-file logging, only recommended when running as a public web app
307
378
  - 'simple' - Logs saved to one file
308
379
  - 'stdout' - Output logs and errors only to stdout
@@ -355,14 +426,14 @@ def run_converter(filename: str,
355
426
  if from_format is not None:
356
427
  check_from_format(filename, from_format, strict=strict)
357
428
  l_run_output.append(get_converter(filename,
358
- to_format,
359
- *args,
360
- from_format=from_format,
361
- download_dir=download_dir,
362
- max_file_size=max_file_size,
363
- log_file=log_file,
364
- log_mode=log_mode,
365
- **converter_kwargs).run())
429
+ to_format,
430
+ *args,
431
+ from_format=from_format,
432
+ download_dir=download_dir,
433
+ max_file_size=max_file_size,
434
+ log_file=log_file,
435
+ log_mode=log_mode,
436
+ **converter_kwargs).run())
366
437
 
367
438
  elif not is_supported_archive(filename):
368
439
  raise base.FileConverterInputException(f"{filename} is an unsupported archive type. Supported types are: "
@@ -473,4 +544,61 @@ def run_converter(filename: str,
473
544
  exception_class = base.FileConverterAbortException
474
545
  raise exception_class(status_code, msg)
475
546
 
547
+ # Log conversion information if in service mode
548
+ service_mode_ev = os.environ.get(const.SERVICE_MODE_EV)
549
+ service_mode = (service_mode_ev is not None) and (service_mode_ev.lower() == "true")
550
+ if service_mode:
551
+ try:
552
+ l_index = filename.rfind('/') + 1
553
+ r_index = len(filename)
554
+ in_filename = filename[l_index:r_index]
555
+
556
+ l_index = run_output.output_filename.rfind('/') + 1
557
+ r_index = len(run_output.output_filename)
558
+
559
+ input_size = set_size_units(run_output.in_size)
560
+ output_size = set_size_units(run_output.out_size)
561
+
562
+ if status_code:
563
+ outcome = "failed"
564
+ fail_reason = l_error_lines
565
+ else:
566
+ outcome = "succeeded"
567
+ fail_reason = ""
568
+
569
+ entry = {
570
+ "datetime": log_utility.get_date_time(),
571
+ "input_format": converter_kwargs['data']['from_full'],
572
+ "output_format": converter_kwargs['data']['to_full'],
573
+ "input_filename": in_filename,
574
+ "output_filename": run_output.output_filename[l_index:r_index],
575
+ "input_size": input_size,
576
+ "output_size": output_size }
577
+
578
+ for key in [ "converter", "coordinates", "coordOption", "from_flags",
579
+ "to_flags", "from_arg_flags", "to_arg_flags" ]:
580
+ if key in converter_kwargs['data'] and converter_kwargs['data'][key] != "" and not \
581
+ ((key == "coordinates" or key == "coordOption") and converter_kwargs['data']['coordinates'] == "neither") :
582
+ entry[key] = converter_kwargs['data'][key]
583
+
584
+ entry["outcome"] = outcome
585
+
586
+ if fail_reason != "":
587
+ entry["fail_reason"] = fail_reason
588
+
589
+ logLock.acquire()
590
+ sys.__stdout__.write(f"{json.dumps(entry) + '\n'}")
591
+ logLock.release()
592
+ except Exception:
593
+ sys.__stdout__.write({"datetime": log_utility.get_date_time(),
594
+ "logging_error": "An error occurred during logging of conversion information."})
595
+
476
596
  return run_output
597
+
598
+ def set_size_units(size):
599
+ if size >= 1024:
600
+ return str('%.3f' % (size / 1024)) + ' kB'
601
+ elif size >= const.MEGABYTE:
602
+ return str(size / const.MEGABYTE) + ' MB'
603
+ else:
604
+ return str(size) + ' B'
@@ -15,8 +15,9 @@ from logging import getLogger
15
15
  from typing import Any, Literal, overload
16
16
 
17
17
  from psdi_data_conversion import constants as const
18
- from psdi_data_conversion.converter import D_REGISTERED_CONVERTERS, D_SUPPORTED_CONVERTERS
18
+ from psdi_data_conversion.converter import D_SUPPORTED_CONVERTERS, get_registered_converter_class
19
19
  from psdi_data_conversion.converters.base import FileConverterException
20
+ from psdi_data_conversion.utils import regularize_name
20
21
 
21
22
  # Keys for top-level and general items in the database
22
23
  DB_FORMATS_KEY = "formats"
@@ -117,14 +118,14 @@ class ConverterInfo:
117
118
  Parameters
118
119
  ----------
119
120
  name : str
120
- The name of the converter
121
+ The regularized name of the converter
121
122
  parent : DataConversionDatabase
122
123
  The database which this belongs to
123
124
  d_data : dict[str, Any]
124
125
  The loaded database dict
125
126
  """
126
127
 
127
- self.name = name
128
+ self.name = regularize_name(name)
128
129
  self.parent = parent
129
130
 
130
131
  # Get info about the converter from the database
@@ -134,7 +135,7 @@ class ConverterInfo:
134
135
 
135
136
  # Get necessary info about the converter from the class
136
137
  try:
137
- self._key_prefix = D_REGISTERED_CONVERTERS[name].database_key_prefix
138
+ self._key_prefix = get_registered_converter_class(name).database_key_prefix
138
139
  except KeyError:
139
140
  # We'll get a KeyError for converters in the database that don't yet have their own class, which we can
140
141
  # safely ignore
@@ -529,6 +530,10 @@ class ConversionQualityInfo:
529
530
  input and output file formats and a note on the implications
530
531
  """
531
532
 
533
+ def __post_init__(self):
534
+ """Regularize the converter name"""
535
+ self.converter_name = regularize_name(self.converter_name)
536
+
532
537
 
533
538
  class ConversionsTable:
534
539
  """Class providing information on available file format conversions.
@@ -623,7 +628,7 @@ class ConversionsTable:
623
628
 
624
629
  # Check if this converter deals with ambiguous formats, so we know if we need to be strict about getting format
625
630
  # info
626
- if D_REGISTERED_CONVERTERS[converter_name].supports_ambiguous_extensions:
631
+ if get_registered_converter_class(converter_name).supports_ambiguous_extensions:
627
632
  which_format = None
628
633
  else:
629
634
  which_format = 0
@@ -805,7 +810,7 @@ class DataConversionDatabase:
805
810
  if self._d_converter_info is None:
806
811
  self._d_converter_info: dict[str, ConverterInfo] = {}
807
812
  for d_single_converter_info in self.converters:
808
- name: str = d_single_converter_info[DB_NAME_KEY]
813
+ name: str = regularize_name(d_single_converter_info[DB_NAME_KEY])
809
814
  if name in self._d_converter_info:
810
815
  logger.warning(f"Converter '{name}' appears more than once in the database. Only the first instance"
811
816
  " will be used.")
@@ -1095,7 +1100,7 @@ def get_converter_info(name: str) -> ConverterInfo:
1095
1100
  ConverterInfo
1096
1101
  """
1097
1102
 
1098
- return get_database().d_converter_info[name]
1103
+ return get_database().d_converter_info[regularize_name(name)]
1099
1104
 
1100
1105
 
1101
1106
  @overload
@@ -1155,7 +1160,7 @@ def get_conversion_quality(converter_name: str,
1155
1160
  `ConversionQualityInfo` object with info on the conversion
1156
1161
  """
1157
1162
 
1158
- return get_database().conversions_table.get_conversion_quality(converter_name=converter_name,
1163
+ return get_database().conversions_table.get_conversion_quality(converter_name=regularize_name(converter_name),
1159
1164
  in_format=in_format,
1160
1165
  out_format=out_format)
1161
1166
 
@@ -1210,6 +1215,9 @@ def disambiguate_formats(converter_name: str,
1210
1215
  If more than one format combination is possible for this conversion, or no conversion is possible
1211
1216
  """
1212
1217
 
1218
+ # Regularize the converter name so we don't worry about case/spacing mismatches
1219
+ converter_name = regularize_name(converter_name)
1220
+
1213
1221
  # Get all possible conversions, and see if we only have one for this converter
1214
1222
  l_possible_conversions = [x for x in get_possible_conversions(in_format, out_format)
1215
1223
  if x[0] == converter_name]
@@ -1243,7 +1251,7 @@ def get_possible_formats(converter_name: str) -> tuple[list[FormatInfo], list[Fo
1243
1251
  tuple[list[FormatInfo], list[FormatInfo]]
1244
1252
  A tuple of a list of the supported input formats and a list of the supported output formats
1245
1253
  """
1246
- return get_database().conversions_table.get_possible_formats(converter_name=converter_name)
1254
+ return get_database().conversions_table.get_possible_formats(converter_name=regularize_name(converter_name))
1247
1255
 
1248
1256
 
1249
1257
  def _find_arg(tl_args: tuple[list[FlagInfo], list[OptionInfo]],
@@ -16,8 +16,10 @@ from itertools import product
16
16
 
17
17
  from psdi_data_conversion import constants as const
18
18
  from psdi_data_conversion.constants import CL_SCRIPT_NAME, CONVERTER_DEFAULT, TERM_WIDTH
19
- from psdi_data_conversion.converter import (D_CONVERTER_ARGS, D_SUPPORTED_CONVERTERS, L_REGISTERED_CONVERTERS,
20
- L_SUPPORTED_CONVERTERS, run_converter)
19
+ from psdi_data_conversion.converter import (D_CONVERTER_ARGS, L_REGISTERED_CONVERTERS, L_SUPPORTED_CONVERTERS,
20
+ converter_is_registered, converter_is_supported,
21
+ get_registered_converter_class, get_supported_converter_class,
22
+ run_converter)
21
23
  from psdi_data_conversion.converters.base import (FileConverterAbortException, FileConverterException,
22
24
  FileConverterInputException)
23
25
  from psdi_data_conversion.database import (FormatInfo, get_conversion_quality, get_converter_info, get_format_info,
@@ -25,6 +27,7 @@ from psdi_data_conversion.database import (FormatInfo, get_conversion_quality, g
25
27
  get_possible_formats)
26
28
  from psdi_data_conversion.file_io import split_archive_ext
27
29
  from psdi_data_conversion.log_utility import get_log_level_from_str
30
+ from psdi_data_conversion.utils import regularize_name
28
31
 
29
32
 
30
33
  def print_wrap(s: str, newline=False, err=False, **kwargs):
@@ -58,9 +61,9 @@ class ConvertArgs:
58
61
  self._output_dir: str | None = args.out
59
62
  converter_name = getattr(args, "with")
60
63
  if isinstance(converter_name, str):
61
- self.name = converter_name
64
+ self.name = regularize_name(converter_name)
62
65
  elif converter_name:
63
- self.name: str = " ".join(converter_name)
66
+ self.name = regularize_name(" ".join(converter_name))
64
67
  else:
65
68
  self.name = None
66
69
  self.delete_input = args.delete_input
@@ -109,14 +112,14 @@ class ConvertArgs:
109
112
 
110
113
  # Get the converter name from the arguments if it wasn't provided by -w/--with
111
114
  if not self.name:
112
- self.name = " ".join(self.l_args)
115
+ self.name = regularize_name(" ".join(self.l_args))
113
116
 
114
117
  # For this operation, any other arguments can be ignored
115
118
  return
116
119
 
117
120
  # If not listing and a converter name wasn't supplied, use the default converter
118
121
  if not self.name:
119
- self.name = CONVERTER_DEFAULT
122
+ self.name = regularize_name(CONVERTER_DEFAULT)
120
123
 
121
124
  # Quiet mode is equivalent to logging mode == LOGGING_NONE, so normalize them if either is set
122
125
  if self.quiet:
@@ -147,13 +150,14 @@ class ConvertArgs:
147
150
  os.makedirs(self._output_dir, exist_ok=True)
148
151
 
149
152
  # Check the converter is recognized
150
- if self.name not in L_SUPPORTED_CONVERTERS:
153
+ if not converter_is_supported(self.name):
151
154
  msg = textwrap.fill(f"ERROR: Converter '{self.name}' not recognised", width=TERM_WIDTH)
152
155
  msg += f"\n\n{get_supported_converters()}"
153
156
  raise FileConverterInputException(msg, help=True, msg_preformatted=True)
154
- elif self.name not in L_REGISTERED_CONVERTERS:
155
- msg = textwrap.fill(f"ERROR: Converter '{self.name}' is not registered. It may be possible to register "
156
- "it by installing an appropriate binary for your platform.", width=TERM_WIDTH)
157
+ elif not converter_is_registered(self.name):
158
+ converter_name = get_supported_converter_class(self.name).name
159
+ msg = textwrap.fill(f"ERROR: Converter '{converter_name}' is not registered. It may be possible to "
160
+ "register it by installing an appropriate binary for your platform.", width=TERM_WIDTH)
157
161
  msg += f"\n\n{get_supported_converters()}"
158
162
  raise FileConverterInputException(msg, help=True, msg_preformatted=True)
159
163
 
@@ -349,9 +353,10 @@ def detail_converter_use(args: ConvertArgs):
349
353
  """
350
354
 
351
355
  converter_info = get_converter_info(args.name)
352
- converter_class = D_SUPPORTED_CONVERTERS[args.name]
356
+ converter_class = get_supported_converter_class(args.name)
357
+ converter_name = converter_class.name
353
358
 
354
- print_wrap(f"{converter_info.name}: {converter_info.description} ({converter_info.url})", break_long_words=False,
359
+ print_wrap(f"{converter_name}: {converter_info.description} ({converter_info.url})", break_long_words=False,
355
360
  break_on_hyphens=False, newline=True)
356
361
 
357
362
  if converter_class.info:
@@ -362,10 +367,10 @@ def detail_converter_use(args: ConvertArgs):
362
367
  if args.from_format is not None and args.to_format is not None:
363
368
  qual = get_conversion_quality(args.name, args.from_format, args.to_format)
364
369
  if qual is None:
365
- print_wrap(f"Conversion from '{args.from_format}' to '{args.to_format}' with {args.name} is not "
370
+ print_wrap(f"Conversion from '{args.from_format}' to '{args.to_format}' with {converter_name} is not "
366
371
  "supported.", newline=True)
367
372
  else:
368
- print_wrap(f"Conversion from '{args.from_format}' to '{args.to_format}' with {args.name} is "
373
+ print_wrap(f"Conversion from '{args.from_format}' to '{args.to_format}' with {converter_name} is "
369
374
  f"possible with {qual.qual_str} conversion quality", newline=True)
370
375
  # If there are any potential issues with the conversion, print them out
371
376
  if qual.details:
@@ -385,7 +390,7 @@ def detail_converter_use(args: ConvertArgs):
385
390
  optional_not: str = ""
386
391
  else:
387
392
  optional_not: str = "not "
388
- print_wrap(f"Conversion {to_or_from} {format_name} is {optional_not}supported by {args.name}.\n")
393
+ print_wrap(f"Conversion {to_or_from} {format_name} is {optional_not}supported by {converter_name}.\n")
389
394
 
390
395
  # List all possible formats, and which can be used for input and which for output
391
396
  s_all_formats: set[FormatInfo] = set(l_input_formats)
@@ -393,7 +398,7 @@ def detail_converter_use(args: ConvertArgs):
393
398
  l_all_formats: list[FormatInfo] = list(s_all_formats)
394
399
  l_all_formats.sort(key=lambda x: x.disambiguated_name.lower())
395
400
 
396
- print_wrap(f"File formats supported by {args.name}:", newline=True)
401
+ print_wrap(f"File formats supported by {converter_name}:", newline=True)
397
402
  max_format_length = max([len(x.disambiguated_name) for x in l_all_formats])
398
403
  print(" "*(max_format_length+4) + " INPUT OUTPUT")
399
404
  print(" "*(max_format_length+4) + " ----- ------")
@@ -472,13 +477,13 @@ def detail_converter_use(args: ConvertArgs):
472
477
  # Now at the end, bring up input/output-format-specific flags and options
473
478
  if mention_input_format and mention_output_format:
474
479
  print_wrap("For details on input/output flags and options allowed for specific formats, call:\n"
475
- f"{CL_SCRIPT_NAME} -l {args.name} -f <input_format> -t <output_format>")
480
+ f"{CL_SCRIPT_NAME} -l {converter_name} -f <input_format> -t <output_format>")
476
481
  elif mention_input_format:
477
482
  print_wrap("For details on input flags and options allowed for a specific format, call:\n"
478
- f"{CL_SCRIPT_NAME} -l {args.name} -f <input_format> [-t <output_format>]")
483
+ f"{CL_SCRIPT_NAME} -l {converter_name} -f <input_format> [-t <output_format>]")
479
484
  elif mention_output_format:
480
485
  print_wrap("For details on output flags and options allowed for a specific format, call:\n"
481
- f"{CL_SCRIPT_NAME} -l {args.name} -t <output_format> [-f <input_format>]")
486
+ f"{CL_SCRIPT_NAME} -l {converter_name} -t <output_format> [-f <input_format>]")
482
487
 
483
488
 
484
489
  def list_supported_formats(err=False):
@@ -600,9 +605,11 @@ def detail_formats_and_possible_converters(from_format: str, to_format: str):
600
605
  l_conversions_matching_formats = [x for x in l_possible_conversions
601
606
  if x[1] == possible_from_format and x[2] == possible_to_format]
602
607
 
603
- l_possible_registered_converters = [x[0] for x in l_conversions_matching_formats
608
+ l_possible_registered_converters = [get_registered_converter_class(x[0]).name
609
+ for x in l_conversions_matching_formats
604
610
  if x[0] in L_REGISTERED_CONVERTERS]
605
- l_possible_unregistered_converters = [x[0] for x in l_conversions_matching_formats
611
+ l_possible_unregistered_converters = [get_supported_converter_class(x[0]).name
612
+ for x in l_conversions_matching_formats
606
613
  if x[0] in L_SUPPORTED_CONVERTERS and x[0] not in L_REGISTERED_CONVERTERS]
607
614
 
608
615
  print()
@@ -640,7 +647,7 @@ def get_supported_converters():
640
647
  l_converters: list[str] = []
641
648
  any_not_registered = False
642
649
  for converter_name in L_SUPPORTED_CONVERTERS:
643
- converter_text = converter_name
650
+ converter_text = get_supported_converter_class(converter_name).name
644
651
  if converter_name not in L_REGISTERED_CONVERTERS:
645
652
  converter_text += f" {MSG_NOT_REGISTERED}"
646
653
  any_not_registered = True
@@ -48,7 +48,7 @@
48
48
  <hr>
49
49
  </div>
50
50
  <ul class="footer__col footer__items clean-list">
51
- <li><a href="https://psdistg.wpengine.com/">PSDI Home</a></li>
51
+ <li><a href="https://psdi.ac.uk/">PSDI Home</a></li>
52
52
  <li><a href="mailto:support@psdi.ac.uk">Contact Us</a></li>
53
53
  <li><a href="https://www.psdi.ac.uk/privacy/">Privacy</a></li>
54
54
  <li><a href="https://www.psdi.ac.uk/terms-and-conditions/">Terms and Conditions</a></li>
@@ -4,7 +4,7 @@
4
4
  <div class="max-width-box navbar">
5
5
  <div class="header-left">
6
6
  <div class="navbar__brand">
7
- <a class="navbar__logo" href="https://psdistg.wpengine.com/">
7
+ <a class="navbar__logo" href="https://psdi.ac.uk/">
8
8
  <img src="static/img/psdi-logo-darktext.png" alt="PSDI logo"
9
9
  class="lm-only">
10
10
  <img src="static/img/psdi-logo-lighttext.png" alt="PSDI logo"
@@ -48,7 +48,7 @@
48
48
  <hr>
49
49
  </div>
50
50
  <ul class="footer__col footer__items clean-list">
51
- <li><a href="https://psdistg.wpengine.com/">PSDI Home</a></li>
51
+ <li><a href="https://psdi.ac.uk/">PSDI Home</a></li>
52
52
  <li><a href="mailto:support@psdi.ac.uk">Contact Us</a></li>
53
53
  <li><a href="https://www.psdi.ac.uk/privacy/">Privacy</a></li>
54
54
  <li><a href="https://www.psdi.ac.uk/terms-and-conditions/">Terms and Conditions</a></li>
@@ -4,7 +4,7 @@
4
4
  <div class="max-width-box navbar">
5
5
  <div class="header-left">
6
6
  <div class="navbar__brand">
7
- <a class="navbar__logo" href="https://psdistg.wpengine.com/">
7
+ <a class="navbar__logo" href="https://psdi.ac.uk/">
8
8
  <img src="../img/psdi-logo-darktext.png" alt="PSDI logo"
9
9
  class="lm-only">
10
10
  <img src="../img/psdi-logo-lighttext.png" alt="PSDI logo"
@@ -20,13 +20,12 @@
20
20
  crossorigin="anonymous"></script>
21
21
  <script src="https://cdn.jsdelivr.net/jquery.dirtyforms/2.0.0/jquery.dirtyforms.min.js"></script>
22
22
 
23
- {% for item in data %}
24
23
  <script>
25
- const token = "{{item.token}}";
26
- const max_file_size = "{{item.max_file_size}}";
27
- const max_file_size_ob = "{{item.max_file_size_ob}}";
28
- const service_mode = "{{item.service_mode}}";
29
- const production_mode = "{{item.production_mode}}";
24
+ const token = "{{token}}";
25
+ const max_file_size = "{{max_file_size}}";
26
+ const max_file_size_ob = "{{max_file_size_ob}}";
27
+ const service_mode = "{{service_mode}}";
28
+ const production_mode = "{{production_mode}}";
30
29
  document.documentElement.setAttribute("service-mode", service_mode);
31
30
  document.documentElement.setAttribute("production-mode", production_mode);
32
31
  </script>
@@ -121,11 +120,11 @@
121
120
  <div class="medGap"></div>
122
121
  </div>
123
122
  </form>
124
-
123
+ {% if tag %}
125
124
  <div class="secondary prod-only">
126
- <div class="max-width-box">SHA: {{item.sha}}</div>
125
+ <div class="max-width-box">PSDI Data Conversion {{ tag }} {% if sha %} (SHA: {{ sha }}) {% endif %}</div>
127
126
  </div>
128
- {% endfor %}
127
+ {% endif %}
129
128
  <script src="{{url_for('static', filename='/javascript/format.js')}}" type="module" language="JavaScript"></script>
130
129
 
131
130
  <footer class="footer" id="psdi-footer"></footer>
@@ -30,7 +30,9 @@ l_all_test_specs.append(Spec(name="Standard Single Test",
30
30
  CheckLogContentsSuccess(),
31
31
  MatchOutputFile("standard_test.inchi")),
32
32
  ))
33
+ """A quick single test, functioning mostly as a smoke test for things going right in the simplest case"""
33
34
 
35
+ simple_success_callback = MCB(CheckFileStatus(), CheckLogContentsSuccess())
34
36
  l_all_test_specs.append(Spec(name="Standard Multiple Tests",
35
37
  filename=["1NE6.mmcif",
36
38
  "hemoglobin.pdb", "aceticacid.mol", "nacl.cif",
@@ -52,8 +54,7 @@ l_all_test_specs.append(Spec(name="Standard Multiple Tests",
52
54
  CONVERTER_ATO, CONVERTER_ATO, CONVERTER_ATO,
53
55
  CONVERTER_C2X, CONVERTER_C2X, CONVERTER_C2X,
54
56
  CONVERTER_OB],
55
- callback=MCB(CheckFileStatus(),
56
- CheckLogContentsSuccess()),
57
+ callback=simple_success_callback,
57
58
  ))
58
59
  """A basic set of test conversions which we expect to succeed without issue, running conversions with each of the
59
60
  Open Babel, Atomsk, and c2x converters"""
@@ -61,14 +62,21 @@ Open Babel, Atomsk, and c2x converters"""
61
62
  l_all_test_specs.append(Spec(name="c2x Formats Tests",
62
63
  to_format=["res", "abi", "POSCAR", "cml"],
63
64
  converter_name=CONVERTER_C2X,
64
- callback=MCB(CheckFileStatus(),
65
- CheckLogContentsSuccess()),
65
+ callback=simple_success_callback,
66
66
  compatible_with_gui=False,
67
67
  ))
68
68
  """Test converting with c2x to a few different formats which require special input. This test isn't run in the GUI
69
69
  solely to save on resources, since there are unlikely to be an GUI-specific issues raised by this test that aren't
70
70
  caught in others."""
71
71
 
72
+ l_all_test_specs.append(Spec(name="Converter Name Sensitivity Tests",
73
+ converter_name=["open babel", "oPeNbaBEL", "C2X", "atomsk"],
74
+ to_format="xyz-0",
75
+ callback=simple_success_callback,
76
+ compatible_with_gui=False,
77
+ ))
78
+ """Tests that converters can be specified case- and space-insensitively in the library and CLI"""
79
+
72
80
  archive_callback = MCB(CheckFileStatus(),
73
81
  CheckArchiveContents(l_filename_bases=["caffeine-no-flags",
74
82
  "caffeine-ia",
@@ -0,0 +1,21 @@
1
+ """
2
+ # utils.py
3
+
4
+ Miscellaneous utility functions used by this project
5
+ """
6
+
7
+
8
+ def regularize_name(name: str):
9
+ """Regularizes a name for comparisons, making it lowercase and stripping spaces
10
+
11
+ Parameters
12
+ ----------
13
+ name : str
14
+ The name, e.g. "Open Babel"
15
+
16
+ Returns
17
+ -------
18
+ str
19
+ The regularized name, e.g. "openbabel"
20
+ """
21
+ return name.lower().replace(" ", "")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: psdi_data_conversion
3
- Version: 0.0.39
3
+ Version: 0.1.1
4
4
  Summary: Chemistry file format conversion service, provided by PSDI
5
5
  Project-URL: Homepage, https://data-conversion.psdi.ac.uk/
6
6
  Project-URL: Documentation, https://psdi-uk.github.io/psdi-data-conversion/
@@ -243,7 +243,7 @@ Description-Content-Type: text/markdown
243
243
 
244
244
  # PSDI Data Conversion
245
245
 
246
- Version: Pre-release 2024-04-14
246
+ Release date: 2024-04-29
247
247
 
248
248
  This is the repository for the PSDI PF2 Chemistry File Format Conversion project. The goal of this project is to provide utilities to assist in converting files between the many different file formats used in chemistry, providing information on what converters are available for a given conversion and the expected quality of it, and providing multiple interfaces to perform these conversions. These interfaces are:
249
249
 
@@ -1,13 +1,14 @@
1
1
  psdi_data_conversion/__init__.py,sha256=urMsTqsTHTch1q4rMT9dgGnrvdPFMP9B8r-6Kr8H5sE,404
2
- psdi_data_conversion/app.py,sha256=A1TUVYuHUJKbtBXC0MbCDwyNcZgCYLDWyt_2lPLETK0,14212
3
- psdi_data_conversion/constants.py,sha256=JWYC2gXsEi6bBl_NdEBh5nLZ7qX2LZZV_DutfEHJ8qo,7390
4
- psdi_data_conversion/converter.py,sha256=A9u_xnzQ_OFp_rttttBNwZNA4d56Fdbm9FpjgyCroUA,23211
5
- psdi_data_conversion/database.py,sha256=tgqUxW75eaWiEsiHNI7pUfMTIKmB23zrprSZ3ZDpO3Y,55967
2
+ psdi_data_conversion/app.py,sha256=jU0cK3yduKQkDECf1UGJCPpBF57bUitM_YHH0vsoS4A,16745
3
+ psdi_data_conversion/constants.py,sha256=Hq2OVbcSkcv6T87-YJlo1PVlr9ILlB4H3E9JYjzvCF4,7423
4
+ psdi_data_conversion/converter.py,sha256=Y77mqH2OKxf2YelphWDl82AKoRa-APle-l3e8wG_WZA,27315
5
+ psdi_data_conversion/database.py,sha256=XzKvtT-cFmKLWLo5OBB5CV_rGHuqEEuENNyZnbS39qE,56419
6
6
  psdi_data_conversion/dist.py,sha256=LOcKEP7H7JA9teX1m-5awuBi69gmdhtUit7yxtCTOZ8,2293
7
7
  psdi_data_conversion/file_io.py,sha256=LvdPmnYL_7Xlcr-7LJjUbbky4gKiqTTvPRzdbtvQaJo,8794
8
8
  psdi_data_conversion/log_utility.py,sha256=CHAq-JvBnTKaE0SHK5hM5j2dTbfSli4iUc3hsf6dBhc,8789
9
- psdi_data_conversion/main.py,sha256=NaV5Pqe-8qYn7ybgok3t6RuRK1H_9ip68UWpV7DRgO0,41773
9
+ psdi_data_conversion/main.py,sha256=9Gu3CxbUfMuDxyNBx_-kQwWB4eOTWRHGmxCsVnpdbvs,42421
10
10
  psdi_data_conversion/security.py,sha256=wjdrMre29TpkF2NqrsXJ5sschSAnDzqLYTLUcNR21Qw,902
11
+ psdi_data_conversion/utils.py,sha256=iTjNfrD4n_hU9h20ldYrX2Bmp5KhCBIeMSUMLtPZ_8k,402
11
12
  psdi_data_conversion/bin/LICENSE_ATOMSK,sha256=-Ay6SFTAf9x-OaRAiOgMNoutfUMLHx5jQQA1HqZ6p7I,34886
12
13
  psdi_data_conversion/bin/LICENSE_C2X,sha256=-Ay6SFTAf9x-OaRAiOgMNoutfUMLHx5jQQA1HqZ6p7I,34886
13
14
  psdi_data_conversion/bin/linux/atomsk,sha256=GDsG1MlEvmk_XPspadzEzuil6N775iewDvNZS6rWJWk,34104032
@@ -29,12 +30,12 @@ psdi_data_conversion/static/content/documentation.htm,sha256=1GiEjlDCP0kJ3CKkx3l
29
30
  psdi_data_conversion/static/content/download.htm,sha256=DQKWEuq_Bjv2TyV6DnPXnHrSOBvGYRHOU-m6YOwfsh4,4727
30
31
  psdi_data_conversion/static/content/feedback.htm,sha256=fZrhn4Egs9g6ygqPzV6ZG9ndYK02VdALNVNXRsZ716k,1788
31
32
  psdi_data_conversion/static/content/header-links.html,sha256=7B2bHa7dLfSZ4hPMvVJ3kR0niVAOO3FtHJo0K6m1oP4,693
32
- psdi_data_conversion/static/content/psdi-common-footer.html,sha256=Qo6ecItnjqngIu1Kyg8YOptqjraMFxyJTYu3wiW5Wuw,4080
33
- psdi_data_conversion/static/content/psdi-common-header.html,sha256=vRAgLGKE_RhXgBUQnarxnpkGl9j2Qeedqqo3VNDVGdk,1825
33
+ psdi_data_conversion/static/content/psdi-common-footer.html,sha256=3_KqTtwtkvCvi52LcRrN7oVfMfFdHqrHc39gMrYzGxg,4070
34
+ psdi_data_conversion/static/content/psdi-common-header.html,sha256=2upXPOZ-EM6Bv7ltPJnynPBjYz4Z27sNl4LuUaVdVjM,1815
34
35
  psdi_data_conversion/static/content/report.htm,sha256=CRwlF7a7QvAjHsajuehWOtLxbo-JAjqrS_Q-I4BWxzs,4549
35
36
  psdi_data_conversion/static/content/index-versions/header-links.html,sha256=WN9oZL7hLtH_yv5PvX3Ky7_YGrNvQL_RQ5eU8DB50Rg,764
36
- psdi_data_conversion/static/content/index-versions/psdi-common-footer.html,sha256=nFZtEObp-X73p95oTOAvqzoPwjsEJD5dKUDhUPDCcj4,4136
37
- psdi_data_conversion/static/content/index-versions/psdi-common-header.html,sha256=IXmvdlArY-60b7HgvKjcNF3eSmEmTImX7Hdasswyi5s,1907
37
+ psdi_data_conversion/static/content/index-versions/psdi-common-footer.html,sha256=APBMI9c5-4fNV-CCYEs9ySgAahc7u1w-0e3EjM1MiD8,4126
38
+ psdi_data_conversion/static/content/index-versions/psdi-common-header.html,sha256=w6Q_uu0W4MdioVoh6w0Cy4m2ACr7xAnampx1TfPvT9M,1897
38
39
  psdi_data_conversion/static/data/data.json,sha256=1nljosxtwbLRfIIIa6-GHJnhvzhB759oEGvVQ18QP_4,3647095
39
40
  psdi_data_conversion/static/img/colormode-toggle-dm.svg,sha256=Q85ODwU67chZ77lyT9gITtnmqzJEycFmz35dJuqaPXE,502
40
41
  psdi_data_conversion/static/img/colormode-toggle-lm.svg,sha256=sIKXsNmLIXU4fSuuqrN0r-J4Hd3NIqoiXNT3mdq5-Fo,1155
@@ -73,15 +74,15 @@ psdi_data_conversion/static/javascript/psdi-common.js,sha256=I0QqGQ7l_rA4KEfenQT
73
74
  psdi_data_conversion/static/javascript/report.js,sha256=BHH5UOhXJtB6J_xk_y6woquNKt5W9hCrQapxKtGG1eA,12470
74
75
  psdi_data_conversion/static/styles/format.css,sha256=PaQkUVxQfXI9nbJ-7YsN1tNIcLXfwXk8wJC-zht8nRA,3269
75
76
  psdi_data_conversion/static/styles/psdi-common.css,sha256=09VY-lldoZCrohuqPKnd9fvDget5g9ybi6uh13pYeY0,17249
76
- psdi_data_conversion/templates/index.htm,sha256=wuHECSzTwVwmaGg55RnYunsaSFCYBmxf6_P4OygTNEQ,6824
77
+ psdi_data_conversion/templates/index.htm,sha256=Y1KlrBirqDPyleVl_Edgbm_jYMBYGnOEMdCzmDBJi3o,6842
77
78
  psdi_data_conversion/testing/__init__.py,sha256=Xku7drtLTYLLPsd403eC0LIEa_iohVifyeyAITy2w7U,135
78
79
  psdi_data_conversion/testing/constants.py,sha256=BtIafruSobZ9cFY0VW5Bu209eiftnN8b3ObouZBrFQU,521
79
80
  psdi_data_conversion/testing/conversion_callbacks.py,sha256=ATR-_BsYCUN8KyOyUjfdWCELzySxLN5jOI0JyrQnmHQ,18858
80
- psdi_data_conversion/testing/conversion_test_specs.py,sha256=jFik-m-jxNZzZhyfiJVIj7CT4ML8pM4dm7Trs2d0hgg,25602
81
+ psdi_data_conversion/testing/conversion_test_specs.py,sha256=8W97tI6dVbHE9BEW76dsKDlfsm5oTlrlntG--b0h8HU,26106
81
82
  psdi_data_conversion/testing/gui.py,sha256=ul7ixYANIzmOG2ZNOZmQO6wsHmGHdiBGAlw-KuoN0j8,19085
82
83
  psdi_data_conversion/testing/utils.py,sha256=YrFxjyiIx1seph0j7jCUgAVm6HvXY9QJjx0MvNJRbfw,26134
83
- psdi_data_conversion-0.0.39.dist-info/METADATA,sha256=QVdwrTA_MdZyiQNOgE2cuVEUh_zA_zs3s9LxQFVY9r4,48116
84
- psdi_data_conversion-0.0.39.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
85
- psdi_data_conversion-0.0.39.dist-info/entry_points.txt,sha256=xL7XTzaPRr2E67WhOD1M1Q-76hB8ausQlnNiHzuZQPA,123
86
- psdi_data_conversion-0.0.39.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
87
- psdi_data_conversion-0.0.39.dist-info/RECORD,,
84
+ psdi_data_conversion-0.1.1.dist-info/METADATA,sha256=M5iHWEennMs8JSqTVqyAKco-9a0a5jRWh6u5W192Ag0,48108
85
+ psdi_data_conversion-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
86
+ psdi_data_conversion-0.1.1.dist-info/entry_points.txt,sha256=xL7XTzaPRr2E67WhOD1M1Q-76hB8ausQlnNiHzuZQPA,123
87
+ psdi_data_conversion-0.1.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
88
+ psdi_data_conversion-0.1.1.dist-info/RECORD,,