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.
- psdi_data_conversion/__init__.py +11 -0
- psdi_data_conversion/app.py +242 -0
- psdi_data_conversion/bin/linux/atomsk +0 -0
- psdi_data_conversion/bin/linux/c2x +0 -0
- psdi_data_conversion/bin/mac/atomsk +0 -0
- psdi_data_conversion/bin/mac/c2x +0 -0
- psdi_data_conversion/constants.py +185 -0
- psdi_data_conversion/converter.py +459 -0
- psdi_data_conversion/converters/__init__.py +6 -0
- psdi_data_conversion/converters/atomsk.py +32 -0
- psdi_data_conversion/converters/base.py +702 -0
- psdi_data_conversion/converters/c2x.py +32 -0
- psdi_data_conversion/converters/openbabel.py +239 -0
- psdi_data_conversion/database.py +1064 -0
- psdi_data_conversion/dist.py +87 -0
- psdi_data_conversion/file_io.py +216 -0
- psdi_data_conversion/log_utility.py +241 -0
- psdi_data_conversion/main.py +776 -0
- psdi_data_conversion/scripts/atomsk.sh +32 -0
- psdi_data_conversion/scripts/c2x.sh +26 -0
- psdi_data_conversion/security.py +38 -0
- psdi_data_conversion/static/content/accessibility.htm +254 -0
- psdi_data_conversion/static/content/convert.htm +121 -0
- psdi_data_conversion/static/content/convertato.htm +65 -0
- psdi_data_conversion/static/content/convertc2x.htm +65 -0
- psdi_data_conversion/static/content/documentation.htm +94 -0
- psdi_data_conversion/static/content/feedback.htm +53 -0
- psdi_data_conversion/static/content/header-links.html +8 -0
- psdi_data_conversion/static/content/index-versions/header-links.html +8 -0
- psdi_data_conversion/static/content/index-versions/psdi-common-footer.html +99 -0
- psdi_data_conversion/static/content/index-versions/psdi-common-header.html +28 -0
- psdi_data_conversion/static/content/psdi-common-footer.html +99 -0
- psdi_data_conversion/static/content/psdi-common-header.html +28 -0
- psdi_data_conversion/static/content/report.htm +103 -0
- psdi_data_conversion/static/data/data.json +143940 -0
- psdi_data_conversion/static/img/colormode-toggle-dm.svg +3 -0
- psdi_data_conversion/static/img/colormode-toggle-lm.svg +3 -0
- psdi_data_conversion/static/img/psdi-icon-dark.svg +136 -0
- psdi_data_conversion/static/img/psdi-icon-light.svg +208 -0
- psdi_data_conversion/static/img/psdi-logo-darktext.png +0 -0
- psdi_data_conversion/static/img/psdi-logo-lighttext.png +0 -0
- psdi_data_conversion/static/img/social-logo-bluesky-black.svg +4 -0
- psdi_data_conversion/static/img/social-logo-bluesky-white.svg +4 -0
- psdi_data_conversion/static/img/social-logo-instagram-black.svg +1 -0
- psdi_data_conversion/static/img/social-logo-instagram-white.svg +1 -0
- psdi_data_conversion/static/img/social-logo-linkedin-black.png +0 -0
- psdi_data_conversion/static/img/social-logo-linkedin-white.png +0 -0
- psdi_data_conversion/static/img/social-logo-mastodon-black.svg +4 -0
- psdi_data_conversion/static/img/social-logo-mastodon-white.svg +4 -0
- psdi_data_conversion/static/img/social-logo-x-black.svg +3 -0
- psdi_data_conversion/static/img/social-logo-x-white.svg +3 -0
- psdi_data_conversion/static/img/social-logo-youtube-black.png +0 -0
- psdi_data_conversion/static/img/social-logo-youtube-white.png +0 -0
- psdi_data_conversion/static/img/ukri-epsr-logo-darktext.png +0 -0
- psdi_data_conversion/static/img/ukri-epsr-logo-lighttext.png +0 -0
- psdi_data_conversion/static/img/ukri-logo-darktext.png +0 -0
- psdi_data_conversion/static/img/ukri-logo-lighttext.png +0 -0
- psdi_data_conversion/static/javascript/accessibility.js +196 -0
- psdi_data_conversion/static/javascript/common.js +42 -0
- psdi_data_conversion/static/javascript/convert.js +296 -0
- psdi_data_conversion/static/javascript/convert_common.js +252 -0
- psdi_data_conversion/static/javascript/convertato.js +107 -0
- psdi_data_conversion/static/javascript/convertc2x.js +107 -0
- psdi_data_conversion/static/javascript/data.js +176 -0
- psdi_data_conversion/static/javascript/format.js +611 -0
- psdi_data_conversion/static/javascript/load_accessibility.js +89 -0
- psdi_data_conversion/static/javascript/psdi-common.js +177 -0
- psdi_data_conversion/static/javascript/report.js +381 -0
- psdi_data_conversion/static/styles/format.css +147 -0
- psdi_data_conversion/static/styles/psdi-common.css +705 -0
- psdi_data_conversion/templates/index.htm +114 -0
- psdi_data_conversion/testing/__init__.py +5 -0
- psdi_data_conversion/testing/constants.py +12 -0
- psdi_data_conversion/testing/conversion_callbacks.py +394 -0
- psdi_data_conversion/testing/conversion_test_specs.py +208 -0
- psdi_data_conversion/testing/utils.py +522 -0
- psdi_data_conversion-0.0.23.dist-info/METADATA +663 -0
- psdi_data_conversion-0.0.23.dist-info/RECORD +81 -0
- psdi_data_conversion-0.0.23.dist-info/WHEEL +4 -0
- psdi_data_conversion-0.0.23.dist-info/entry_points.txt +2 -0
- psdi_data_conversion-0.0.23.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,1064 @@
|
|
1
|
+
"""@file psdi_data_conversion/database.py
|
2
|
+
|
3
|
+
Created 2025-02-03 by Bryan Gillis.
|
4
|
+
|
5
|
+
Python module provide utilities for accessing the converter database
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
from dataclasses import dataclass, field
|
11
|
+
import json
|
12
|
+
from logging import getLogger
|
13
|
+
import os
|
14
|
+
from typing import Any
|
15
|
+
|
16
|
+
from psdi_data_conversion import constants as const
|
17
|
+
from psdi_data_conversion.converter import D_REGISTERED_CONVERTERS
|
18
|
+
from psdi_data_conversion.converters.base import FileConverterException
|
19
|
+
|
20
|
+
# Keys for top-level and general items in the database
|
21
|
+
DB_FORMATS_KEY = "formats"
|
22
|
+
DB_CONVERTERS_KEY = "converters"
|
23
|
+
DB_CONVERTS_TO_KEY = "converts_to"
|
24
|
+
DB_ID_KEY = "id"
|
25
|
+
DB_NAME_KEY = "name"
|
26
|
+
|
27
|
+
# Keys for converter general info in the database
|
28
|
+
DB_DESC_KEY = "description"
|
29
|
+
DB_INFO_KEY = "further_info"
|
30
|
+
DB_URL_KEY = "url"
|
31
|
+
|
32
|
+
# Keys for format general info in the database
|
33
|
+
DB_FORMAT_EXT_KEY = "extension"
|
34
|
+
DB_FORMAT_NOTE_KEY = "note"
|
35
|
+
DB_FORMAT_COMP_KEY = "composition"
|
36
|
+
DB_FORMAT_CONN_KEY = "connections"
|
37
|
+
DB_FORMAT_2D_KEY = "two_dim"
|
38
|
+
DB_FORMAT_3D_KEY = "three_dim"
|
39
|
+
|
40
|
+
# Keys for converts_to info in the database
|
41
|
+
DB_CONV_ID_KEY = "converters_id"
|
42
|
+
DB_IN_ID_KEY = "in_id"
|
43
|
+
DB_OUT_ID_KEY = "out_id"
|
44
|
+
DB_SUCCESS_KEY = "degree_of_success"
|
45
|
+
|
46
|
+
# Key bases for converter-specific items in the database
|
47
|
+
DB_IN_FLAGS_KEY_BASE = "flags_in"
|
48
|
+
DB_OUT_FLAGS_KEY_BASE = "flags_out"
|
49
|
+
DB_IN_OPTIONS_KEY_BASE = "argflags_in"
|
50
|
+
DB_OUT_OPTIONS_KEY_BASE = "argflags_out"
|
51
|
+
DB_IN_FLAGS_FORMATS_KEY_BASE = "format_to_flags_in"
|
52
|
+
DB_OUT_FLAGS_FORMATS_KEY_BASE = "format_to_flags_out"
|
53
|
+
DB_IN_OPTIONS_FORMATS_KEY_BASE = "format_to_argflags_in"
|
54
|
+
DB_OUT_OPTIONS_FORMATS_KEY_BASE = "format_to_argflags_out"
|
55
|
+
|
56
|
+
# Keys for argument info in the database
|
57
|
+
DB_FLAG_KEY = "flag"
|
58
|
+
DB_BRIEF_KEY = "brief"
|
59
|
+
DB_FORMAT_ID_KEY = "formats_id"
|
60
|
+
DB_IN_FLAGS_ID_KEY_BASE = "flags_in_id"
|
61
|
+
DB_OUT_FLAGS_ID_KEY_BASE = "flags_out_id"
|
62
|
+
DB_IN_OPTIONS_ID_KEY_BASE = "argflags_in_id"
|
63
|
+
DB_OUT_OPTIONS_ID_KEY_BASE = "argflags_out_id"
|
64
|
+
|
65
|
+
logger = getLogger(__name__)
|
66
|
+
|
67
|
+
|
68
|
+
class FileConverterDatabaseException(FileConverterException):
|
69
|
+
"""Class for any exceptions which arise from issues with the database classes and methods
|
70
|
+
"""
|
71
|
+
pass
|
72
|
+
|
73
|
+
|
74
|
+
@dataclass
|
75
|
+
class ArgInfo:
|
76
|
+
"""Class providing information on an argument accepted by a converter (whether it accepts a value or not)
|
77
|
+
"""
|
78
|
+
|
79
|
+
parent: ConverterInfo
|
80
|
+
id: int
|
81
|
+
flag: str
|
82
|
+
description: str
|
83
|
+
info: str
|
84
|
+
|
85
|
+
s_in_formats: set[int] = field(default_factory=set)
|
86
|
+
s_out_formats: set[int] = field(default_factory=set)
|
87
|
+
|
88
|
+
|
89
|
+
@dataclass
|
90
|
+
class FlagInfo(ArgInfo):
|
91
|
+
"""Class providing information on a flag accepted by a converter (an argument which doesn't accept a value)
|
92
|
+
"""
|
93
|
+
pass
|
94
|
+
|
95
|
+
|
96
|
+
@dataclass
|
97
|
+
class OptionInfo(ArgInfo):
|
98
|
+
"""Class providing information on an option accepted by a converter (an argument accepts a value)
|
99
|
+
"""
|
100
|
+
# We need to provide a default argument here, since it will come after the sets with default arguments in ArgInfo
|
101
|
+
brief: str = ""
|
102
|
+
|
103
|
+
|
104
|
+
class ConverterInfo:
|
105
|
+
"""Class providing information on a converter stored in the PSDI Data Conversion database
|
106
|
+
"""
|
107
|
+
|
108
|
+
def __init__(self,
|
109
|
+
name: str,
|
110
|
+
parent: DataConversionDatabase,
|
111
|
+
d_single_converter_info: dict[str, int | str],
|
112
|
+
d_data: dict[str, Any]):
|
113
|
+
"""Set up the class - this will be initialised within a `DataConversionDatabase`, which we set as the parent
|
114
|
+
|
115
|
+
Parameters
|
116
|
+
----------
|
117
|
+
name : str
|
118
|
+
The name of the converter
|
119
|
+
parent : DataConversionDatabase
|
120
|
+
The database which this belongs to
|
121
|
+
d_data : dict[str, Any]
|
122
|
+
The loaded database dict
|
123
|
+
"""
|
124
|
+
|
125
|
+
self.name = name
|
126
|
+
self.parent = parent
|
127
|
+
|
128
|
+
# Get info about the converter from the database
|
129
|
+
self.id: int = d_single_converter_info.get(DB_ID_KEY, -1)
|
130
|
+
self.description: str = d_single_converter_info.get(DB_DESC_KEY, "")
|
131
|
+
self.url: str = d_single_converter_info.get(DB_URL_KEY, "")
|
132
|
+
|
133
|
+
# Get necessary info about the converter from the class
|
134
|
+
try:
|
135
|
+
self._key_prefix = D_REGISTERED_CONVERTERS[name].database_key_prefix
|
136
|
+
except KeyError:
|
137
|
+
# We'll get a KeyError for converters in the database that don't yet have their own class, which we can
|
138
|
+
# safely ignore
|
139
|
+
self._key_prefix = None
|
140
|
+
|
141
|
+
self._arg_info: dict[str, list[dict[str, int | str]]] = {}
|
142
|
+
|
143
|
+
# Placeholders for members that are generated when needed
|
144
|
+
self._l_in_flag_info: list[FlagInfo] | None = None
|
145
|
+
self._l_out_flag_info: list[FlagInfo] | None = None
|
146
|
+
self._l_in_option_info: list[OptionInfo] | None = None
|
147
|
+
self._l_out_option_info: list[OptionInfo] | None = None
|
148
|
+
|
149
|
+
self._d_in_format_flags: dict[str | int, set[str]] | None = None
|
150
|
+
self._d_out_format_flags: dict[str | int, set[str]] | None = None
|
151
|
+
self._d_in_format_options: dict[str | int, set[str]] | None = None
|
152
|
+
self._d_out_format_options: dict[str | int, set[str]] | None = None
|
153
|
+
|
154
|
+
# If the converter class has no defined key prefix, don't add any extra info for it
|
155
|
+
if self._key_prefix is None:
|
156
|
+
return
|
157
|
+
for key_base in (DB_IN_FLAGS_KEY_BASE,
|
158
|
+
DB_OUT_FLAGS_KEY_BASE,
|
159
|
+
DB_IN_OPTIONS_KEY_BASE,
|
160
|
+
DB_OUT_OPTIONS_KEY_BASE,
|
161
|
+
DB_IN_FLAGS_FORMATS_KEY_BASE,
|
162
|
+
DB_OUT_FLAGS_FORMATS_KEY_BASE,
|
163
|
+
DB_IN_OPTIONS_FORMATS_KEY_BASE,
|
164
|
+
DB_OUT_OPTIONS_FORMATS_KEY_BASE):
|
165
|
+
self._arg_info[key_base] = d_data.get(self._key_prefix + key_base)
|
166
|
+
|
167
|
+
def _create_l_arg_info(self, subclass: type[ArgInfo]) -> tuple[list[ArgInfo], list[ArgInfo]]:
|
168
|
+
"""Creates either the flag or option info list
|
169
|
+
"""
|
170
|
+
|
171
|
+
# Set values based on whether we're working with flags or options
|
172
|
+
if issubclass(subclass, FlagInfo):
|
173
|
+
in_key_base = DB_IN_FLAGS_KEY_BASE
|
174
|
+
out_key_base = DB_OUT_FLAGS_KEY_BASE
|
175
|
+
in_formats_key_base = DB_IN_FLAGS_FORMATS_KEY_BASE
|
176
|
+
in_args_id_key_base = DB_IN_FLAGS_ID_KEY_BASE
|
177
|
+
out_formats_key_base = DB_OUT_FLAGS_FORMATS_KEY_BASE
|
178
|
+
out_args_id_key_base = DB_OUT_FLAGS_ID_KEY_BASE
|
179
|
+
elif issubclass(subclass, OptionInfo):
|
180
|
+
in_key_base = DB_IN_OPTIONS_KEY_BASE
|
181
|
+
out_key_base = DB_OUT_OPTIONS_KEY_BASE
|
182
|
+
in_formats_key_base = DB_IN_OPTIONS_FORMATS_KEY_BASE
|
183
|
+
in_args_id_key_base = DB_IN_OPTIONS_ID_KEY_BASE
|
184
|
+
out_formats_key_base = DB_OUT_OPTIONS_FORMATS_KEY_BASE
|
185
|
+
out_args_id_key_base = DB_OUT_OPTIONS_ID_KEY_BASE
|
186
|
+
else:
|
187
|
+
raise FileConverterDatabaseException(f"Unrecognised subclass passed to `_create_l_arg_info`: {subclass}")
|
188
|
+
|
189
|
+
for key_base, in_or_out in ((in_key_base, "in"),
|
190
|
+
(out_key_base, "out")):
|
191
|
+
|
192
|
+
max_id = max([x[DB_ID_KEY] for x in self._arg_info[key_base]])
|
193
|
+
l_arg_info: list[ArgInfo] = [None]*(max_id+1)
|
194
|
+
|
195
|
+
for d_single_arg_info in self._arg_info[key_base]:
|
196
|
+
name: str = d_single_arg_info[DB_FLAG_KEY]
|
197
|
+
arg_id: int = d_single_arg_info[DB_ID_KEY]
|
198
|
+
brief = d_single_arg_info.get(DB_BRIEF_KEY)
|
199
|
+
optional_arg_info_kwargs = {}
|
200
|
+
if brief is not None:
|
201
|
+
optional_arg_info_kwargs["brief"] = brief
|
202
|
+
arg_info = subclass(parent=self,
|
203
|
+
id=arg_id,
|
204
|
+
flag=name,
|
205
|
+
description=d_single_arg_info[DB_DESC_KEY],
|
206
|
+
info=d_single_arg_info[DB_INFO_KEY],
|
207
|
+
**optional_arg_info_kwargs)
|
208
|
+
l_arg_info[arg_id] = arg_info
|
209
|
+
|
210
|
+
# Get a list of all in and formats applicable to this flag, and add them to the flag info's sets
|
211
|
+
if in_or_out == "in":
|
212
|
+
l_in_formats = [x[DB_FORMAT_ID_KEY]
|
213
|
+
for x in self._arg_info[in_formats_key_base]
|
214
|
+
if x[self._key_prefix + in_args_id_key_base] == arg_id]
|
215
|
+
arg_info.s_in_formats.update(l_in_formats)
|
216
|
+
else:
|
217
|
+
l_out_formats = [x[DB_FORMAT_ID_KEY]
|
218
|
+
for x in self._arg_info[out_formats_key_base]
|
219
|
+
if x[self._key_prefix + out_args_id_key_base] == arg_id]
|
220
|
+
arg_info.s_out_formats.update(l_out_formats)
|
221
|
+
|
222
|
+
if in_or_out == "in":
|
223
|
+
l_in_arg_info = l_arg_info
|
224
|
+
else:
|
225
|
+
l_out_arg_info = l_arg_info
|
226
|
+
|
227
|
+
return l_in_arg_info, l_out_arg_info
|
228
|
+
|
229
|
+
@property
|
230
|
+
def l_in_flag_info(self) -> list[FlagInfo | None]:
|
231
|
+
"""Generate the input flag info list (indexed by ID) when needed. Returns None if the converter has no flag info
|
232
|
+
in the database
|
233
|
+
"""
|
234
|
+
if self._l_in_flag_info is None and self._key_prefix is not None:
|
235
|
+
self._l_in_flag_info, self._l_out_flag_info = self._create_l_arg_info(FlagInfo)
|
236
|
+
return self._l_in_flag_info
|
237
|
+
|
238
|
+
@property
|
239
|
+
def l_out_flag_info(self) -> list[FlagInfo | None]:
|
240
|
+
"""Generate the output flag info list (indexed by ID) when needed. Returns None if the converter has no flag
|
241
|
+
info in the database
|
242
|
+
"""
|
243
|
+
if self._l_out_flag_info is None and self._key_prefix is not None:
|
244
|
+
self._l_in_flag_info, self._l_out_flag_info = self._create_l_arg_info(FlagInfo)
|
245
|
+
return self._l_out_flag_info
|
246
|
+
|
247
|
+
@property
|
248
|
+
def l_in_option_info(self) -> list[OptionInfo | None]:
|
249
|
+
"""Generate the input option info list (indexed by ID) when needed. Returns None if the converter has no option
|
250
|
+
info in the database
|
251
|
+
"""
|
252
|
+
if self._l_in_option_info is None and self._key_prefix is not None:
|
253
|
+
self._l_in_option_info, self._l_out_option_info = self._create_l_arg_info(OptionInfo)
|
254
|
+
return self._l_in_option_info
|
255
|
+
|
256
|
+
@property
|
257
|
+
def l_out_option_info(self) -> list[OptionInfo | None]:
|
258
|
+
"""Generate the output option info list (indexed by ID) when needed. Returns None if the converter has no option
|
259
|
+
info in the database
|
260
|
+
"""
|
261
|
+
if self._l_out_option_info is None and self._key_prefix is not None:
|
262
|
+
self._l_in_option_info, self._l_out_option_info = self._create_l_arg_info(OptionInfo)
|
263
|
+
return self._l_out_option_info
|
264
|
+
|
265
|
+
def _create_d_format_args(self,
|
266
|
+
subclass: type[ArgInfo],
|
267
|
+
in_or_out: str) -> dict[str | int, set[int]]:
|
268
|
+
"""Creates either the flag or option format args dict
|
269
|
+
"""
|
270
|
+
|
271
|
+
if in_or_out not in ("in", "out"):
|
272
|
+
raise FileConverterDatabaseException(
|
273
|
+
f"Unrecognised `in_or_out` value passed to `_create_d_format_args`: {in_or_out}")
|
274
|
+
|
275
|
+
# Set values based on whether we're working with flags or options, and input or output
|
276
|
+
if issubclass(subclass, FlagInfo):
|
277
|
+
l_arg_info = self.l_in_flag_info if in_or_out == "in" else self.l_out_flag_info
|
278
|
+
elif issubclass(subclass, OptionInfo):
|
279
|
+
l_arg_info = self.l_in_option_info if in_or_out == "in" else self.l_out_option_info
|
280
|
+
else:
|
281
|
+
raise FileConverterDatabaseException(
|
282
|
+
f"Unrecognised subclass passed to `_create_d_format_args`: {subclass}")
|
283
|
+
|
284
|
+
d_format_args: dict[str | int, set[ArgInfo]] = {}
|
285
|
+
l_parent_format_info = self.parent.l_format_info
|
286
|
+
|
287
|
+
for arg_info in l_arg_info:
|
288
|
+
|
289
|
+
if arg_info is None:
|
290
|
+
continue
|
291
|
+
|
292
|
+
if in_or_out == "in":
|
293
|
+
s_formats = arg_info.s_in_formats
|
294
|
+
else:
|
295
|
+
s_formats = arg_info.s_out_formats
|
296
|
+
l_format_info = [l_parent_format_info[format_id] for format_id in s_formats]
|
297
|
+
for format_info in l_format_info:
|
298
|
+
format_name = format_info.name
|
299
|
+
format_id = format_info.id
|
300
|
+
|
301
|
+
# Add an empty set for this format to the dict if it isn't yet there, otherwise add to the set
|
302
|
+
if format_name not in d_format_args:
|
303
|
+
d_format_args[format_name] = set()
|
304
|
+
# Keying by ID will point to the same set as keying by name
|
305
|
+
d_format_args[format_id] = d_format_args[format_name]
|
306
|
+
|
307
|
+
d_format_args[format_name].add(arg_info.id)
|
308
|
+
|
309
|
+
return d_format_args
|
310
|
+
|
311
|
+
@property
|
312
|
+
def d_in_format_flags(self) -> dict[str | int, set[int]]:
|
313
|
+
"""Generate the dict of flags for an input format (keyed by format name/extension or format ID) when needed.
|
314
|
+
The format will not be in the dict if no flags are accepted
|
315
|
+
"""
|
316
|
+
if self._d_in_format_flags is None:
|
317
|
+
self._d_in_format_flags = self._create_d_format_args(FlagInfo, "in")
|
318
|
+
return self._d_in_format_flags
|
319
|
+
|
320
|
+
@property
|
321
|
+
def d_out_format_flags(self) -> dict[str | int, set[int]]:
|
322
|
+
"""Generate the dict of flags for an output format (keyed by format name/extension or format ID) when needed.
|
323
|
+
The format will not be in the dict if no options are accepted
|
324
|
+
"""
|
325
|
+
if self._d_out_format_flags is None:
|
326
|
+
self._d_out_format_flags = self._create_d_format_args(FlagInfo, "out")
|
327
|
+
return self._d_out_format_flags
|
328
|
+
|
329
|
+
@property
|
330
|
+
def d_in_format_options(self) -> dict[str | int, set[int]]:
|
331
|
+
"""Generate the dict of options for an input format (keyed by format name/extension or format ID) when needed.
|
332
|
+
The format will not be in the dict if no options are accepted
|
333
|
+
"""
|
334
|
+
if self._d_in_format_options is None:
|
335
|
+
self._d_in_format_options = self._create_d_format_args(OptionInfo, "in")
|
336
|
+
return self._d_in_format_options
|
337
|
+
|
338
|
+
@property
|
339
|
+
def d_out_format_options(self) -> dict[str | int, set[int]]:
|
340
|
+
"""Generate the dict of options for an output format (keyed by format name/extension or format ID) when needed.
|
341
|
+
The format will not be in the dict if no options are accepted
|
342
|
+
"""
|
343
|
+
if self._d_out_format_options is None:
|
344
|
+
self._d_out_format_options = self._create_d_format_args(OptionInfo, "out")
|
345
|
+
return self._d_out_format_options
|
346
|
+
|
347
|
+
def get_in_format_args(self, name: str) -> tuple[list[FlagInfo], list[OptionInfo]]:
|
348
|
+
"""Get the input flags and options supported for a given format (provided as its extension)
|
349
|
+
|
350
|
+
Parameters
|
351
|
+
----------
|
352
|
+
name : str
|
353
|
+
The file format name (extension)
|
354
|
+
|
355
|
+
Returns
|
356
|
+
-------
|
357
|
+
tuple[set[FlagInfo], set[OptionInfo]]
|
358
|
+
A set of info for the allowed flags, and a set of info for the allowed options
|
359
|
+
"""
|
360
|
+
l_flag_ids = list(self.d_in_format_flags.get(name, set()))
|
361
|
+
l_flag_ids.sort()
|
362
|
+
l_flag_info = [self.l_in_flag_info[x] for x in l_flag_ids]
|
363
|
+
|
364
|
+
l_option_ids = list(self.d_in_format_options.get(name, set()))
|
365
|
+
l_option_ids.sort()
|
366
|
+
l_option_info = [self.l_in_option_info[x] for x in l_option_ids]
|
367
|
+
|
368
|
+
return l_flag_info, l_option_info
|
369
|
+
|
370
|
+
def get_out_format_args(self, name: str) -> tuple[list[FlagInfo], list[OptionInfo]]:
|
371
|
+
"""Get the output flags and options supported for a given format (provided as its extension)
|
372
|
+
|
373
|
+
Parameters
|
374
|
+
----------
|
375
|
+
name : str
|
376
|
+
The file format name (extension)
|
377
|
+
|
378
|
+
Returns
|
379
|
+
-------
|
380
|
+
tuple[set[FlagInfo], set[OptionInfo]]
|
381
|
+
A set of info for the allowed flags, and a set of info for the allowed options
|
382
|
+
"""
|
383
|
+
l_flag_ids = list(self.d_out_format_flags.get(name, set()))
|
384
|
+
l_flag_ids.sort()
|
385
|
+
l_flag_info = [self.l_out_flag_info[x] for x in l_flag_ids]
|
386
|
+
|
387
|
+
l_option_ids = list(self.d_out_format_options.get(name, set()))
|
388
|
+
l_option_ids.sort()
|
389
|
+
l_option_info = [self.l_out_option_info[x] for x in l_option_ids]
|
390
|
+
|
391
|
+
return l_flag_info, l_option_info
|
392
|
+
|
393
|
+
|
394
|
+
class FormatInfo:
|
395
|
+
"""Class providing information on a file format from the PSDI Data Conversion database
|
396
|
+
"""
|
397
|
+
|
398
|
+
def __init__(self,
|
399
|
+
name: str,
|
400
|
+
parent: DataConversionDatabase,
|
401
|
+
d_single_format_info: dict[str, bool | int | str | None]):
|
402
|
+
"""Set up the class - this will be initialised within a `DataConversionDatabase`, which we set as the parent
|
403
|
+
|
404
|
+
Parameters
|
405
|
+
----------
|
406
|
+
name : str
|
407
|
+
The name (extension) of the file format
|
408
|
+
parent : DataConversionDatabase
|
409
|
+
The database which this belongs to
|
410
|
+
d_single_format_info : dict[str, bool | int | str | None]
|
411
|
+
The dict of info on the format stored in the database
|
412
|
+
"""
|
413
|
+
|
414
|
+
# Load attributes from input
|
415
|
+
self.name = name
|
416
|
+
self.parent = parent
|
417
|
+
|
418
|
+
# Load attributes from the database
|
419
|
+
self.id: int = d_single_format_info.get(DB_ID_KEY, -1)
|
420
|
+
self.note: str = d_single_format_info.get(DB_FORMAT_NOTE_KEY, "")
|
421
|
+
self.composition = d_single_format_info.get(DB_FORMAT_COMP_KEY)
|
422
|
+
self.connections = d_single_format_info.get(DB_FORMAT_CONN_KEY)
|
423
|
+
self.two_dim = d_single_format_info.get(DB_FORMAT_2D_KEY)
|
424
|
+
self.three_dim = d_single_format_info.get(DB_FORMAT_3D_KEY)
|
425
|
+
|
426
|
+
|
427
|
+
@dataclass
|
428
|
+
class PropertyConversionInfo:
|
429
|
+
"""Class representing whether a given property is present in the input and/out output file formats, and a note on
|
430
|
+
what its presence or absence means
|
431
|
+
"""
|
432
|
+
key: str
|
433
|
+
input_supported: bool | None
|
434
|
+
output_supported: bool | None
|
435
|
+
label: str = field(init=False)
|
436
|
+
note: str = field(init=False)
|
437
|
+
|
438
|
+
def __post_init__(self):
|
439
|
+
"""Set the label and note based on input/output status
|
440
|
+
"""
|
441
|
+
self.label = const.D_QUAL_LABELS[self.key]
|
442
|
+
|
443
|
+
if self.input_supported is None and self.output_supported is None:
|
444
|
+
self.note = const.QUAL_NOTE_BOTH_UNKNOWN
|
445
|
+
elif self.input_supported is None and self.output_supported is not None:
|
446
|
+
self.note = const.QUAL_NOTE_IN_UNKNOWN
|
447
|
+
elif self.input_supported is not None and self.output_supported is None:
|
448
|
+
self.note = const.QUAL_NOTE_OUT_UNKNOWN
|
449
|
+
elif self.input_supported == self.output_supported:
|
450
|
+
self.note = ""
|
451
|
+
elif self.input_supported:
|
452
|
+
self.note = const.QUAL_NOTE_OUT_MISSING
|
453
|
+
else:
|
454
|
+
self.note = const.QUAL_NOTE_IN_MISSING
|
455
|
+
|
456
|
+
if self.note:
|
457
|
+
self.note = self.note.format(self.label)
|
458
|
+
|
459
|
+
|
460
|
+
@dataclass
|
461
|
+
class ConversionQualityInfo:
|
462
|
+
"""Class describing the quality of a conversion from one format to another with a given converter.
|
463
|
+
"""
|
464
|
+
|
465
|
+
converter_name: str
|
466
|
+
"""The name of the converter"""
|
467
|
+
|
468
|
+
in_format: str
|
469
|
+
"""The extension of the input file format"""
|
470
|
+
|
471
|
+
out_format: str
|
472
|
+
"""The extension of the output file format"""
|
473
|
+
|
474
|
+
qual_str: str
|
475
|
+
"""A string describing the quality of the conversion"""
|
476
|
+
|
477
|
+
details: str
|
478
|
+
"""A string providing details on any possible issues with the conversion"""
|
479
|
+
|
480
|
+
d_prop_conversion_info: dict[str, PropertyConversionInfo]
|
481
|
+
"""A dict of PropertyConversionInfo objects, which provide information on each property's support in the
|
482
|
+
input and output file formats and a note on the implications
|
483
|
+
"""
|
484
|
+
|
485
|
+
|
486
|
+
class ConversionsTable:
|
487
|
+
"""Class providing information on available file format conversions.
|
488
|
+
|
489
|
+
Information on internal data handling of this class:
|
490
|
+
|
491
|
+
The idea here is that we need to be able to get information on whether a converter can handle a conversion from one
|
492
|
+
file format to another. This results in 3D data storage, with dimensions: Converter, Input Format, Output Format.
|
493
|
+
The most important operations are (in roughly descending order of importance):
|
494
|
+
|
495
|
+
- For a given Converter, Input Format, and Output Format, get whether or not the conversion is possible, and the
|
496
|
+
degree of success if it is possible.
|
497
|
+
- For a given Input Format and Output Format, list available Converters and their degrees of success
|
498
|
+
- For a given Converter, list available Input Formats and Output Formats
|
499
|
+
- For a given Input Format, list available Output Formats and Converters, and the degree of success of each
|
500
|
+
|
501
|
+
At date of implementation, the data comprises 9 Converters and 280 Input/Output Formats, for 705,600 possibilities,
|
502
|
+
increasing linearly with the number of converters and quadratically with the number of formats. (Self-to-self format
|
503
|
+
conversions don't need to be stored, but this may not be a useful optimisation.)
|
504
|
+
|
505
|
+
Conversion data is available for 23,013 Converter, Input, Output values, or ~3% of the total possible conversions.
|
506
|
+
While this could currently work as a sparse array, it will likely be filled to become denser over time, so a dense
|
507
|
+
representation makes the most sense.
|
508
|
+
|
509
|
+
The present implementation uses a list-of-lists-of-lists approach, to avoid adding NumPy as a dependency
|
510
|
+
until/unless efficiency concerns motivate it in the future.
|
511
|
+
"""
|
512
|
+
|
513
|
+
def __init__(self,
|
514
|
+
l_converts_to: list[dict[str, bool | int | str | None]],
|
515
|
+
parent: DataConversionDatabase):
|
516
|
+
"""Set up the class - this will be initialised within a `DataConversionDatabase`, which we set as the parent
|
517
|
+
|
518
|
+
Parameters
|
519
|
+
----------
|
520
|
+
l_converts_to : list[dict[str, bool | int | str | None]]
|
521
|
+
The list of dicts in the database providing information on possible conversions
|
522
|
+
parent : DataConversionDatabase
|
523
|
+
The database which this belongs to
|
524
|
+
|
525
|
+
Raises
|
526
|
+
------
|
527
|
+
FileConverterDatabaseException
|
528
|
+
_description_
|
529
|
+
"""
|
530
|
+
|
531
|
+
self.parent = parent
|
532
|
+
|
533
|
+
# Store references to needed data
|
534
|
+
self._l_converts_to = l_converts_to
|
535
|
+
|
536
|
+
# Build the conversion table, indexed Converter, Input Format, Output Format - note that each of these is
|
537
|
+
# 1-indexed, so we add 1 to each of the lengths here
|
538
|
+
num_converters = len(parent.converters)
|
539
|
+
num_formats = len(parent.formats)
|
540
|
+
|
541
|
+
self._table = [[[0 for k in range(num_formats+1)] for j in range(num_formats+1)]
|
542
|
+
for i in range(num_converters+1)]
|
543
|
+
|
544
|
+
for possible_conversion in l_converts_to:
|
545
|
+
|
546
|
+
try:
|
547
|
+
conv_id: int = possible_conversion[DB_CONV_ID_KEY]
|
548
|
+
in_id: int = possible_conversion[DB_IN_ID_KEY]
|
549
|
+
out_id: int = possible_conversion[DB_OUT_ID_KEY]
|
550
|
+
except KeyError:
|
551
|
+
raise FileConverterDatabaseException(
|
552
|
+
f"Malformed 'converts_to' entry in database: {possible_conversion}")
|
553
|
+
|
554
|
+
self._table[conv_id][in_id][out_id] = 1
|
555
|
+
|
556
|
+
def get_conversion_quality(self,
|
557
|
+
converter_name: str,
|
558
|
+
in_format: str,
|
559
|
+
out_format: str) -> ConversionQualityInfo | None:
|
560
|
+
"""Get an indication of the quality of a conversion from one format to another, or if it's not possible
|
561
|
+
|
562
|
+
Parameters
|
563
|
+
----------
|
564
|
+
converter_name : str
|
565
|
+
The name of the converter
|
566
|
+
in_format : str
|
567
|
+
The extension of the input file format
|
568
|
+
out_format : str
|
569
|
+
The extension of the output file format
|
570
|
+
|
571
|
+
Returns
|
572
|
+
-------
|
573
|
+
ConversionQualityInfo | None
|
574
|
+
If the conversion is not possible, returns None. If the conversion is possible, returns a
|
575
|
+
`ConversionQualityInfo` object with info on the conversion
|
576
|
+
"""
|
577
|
+
|
578
|
+
conv_id: int = self.parent.get_converter_info(converter_name).id
|
579
|
+
in_info = self.parent.get_format_info(in_format)
|
580
|
+
out_info: int = self.parent.get_format_info(out_format)
|
581
|
+
|
582
|
+
# First check if the conversion is possible
|
583
|
+
success_flag = self._table[conv_id][in_info.id][out_info.id]
|
584
|
+
if not success_flag:
|
585
|
+
return None
|
586
|
+
|
587
|
+
# The conversion is possible. Now determine how many properties of the output format are not in the input
|
588
|
+
# format and might end up being extrapolated
|
589
|
+
num_out_props = 0
|
590
|
+
num_new_props = 0
|
591
|
+
any_unknown = False
|
592
|
+
d_prop_conversion_info: dict[str, PropertyConversionInfo] = {}
|
593
|
+
for prop in const.D_QUAL_LABELS:
|
594
|
+
in_prop: bool | None = getattr(in_info, prop)
|
595
|
+
out_prop: bool | None = getattr(out_info, prop)
|
596
|
+
|
597
|
+
d_prop_conversion_info[prop] = PropertyConversionInfo(prop, in_prop, out_prop)
|
598
|
+
|
599
|
+
# Check for None, indicating we don't have full information on both formats
|
600
|
+
if in_prop is None or out_prop is None:
|
601
|
+
any_unknown = True
|
602
|
+
elif out_prop:
|
603
|
+
num_out_props += 1
|
604
|
+
if not in_prop:
|
605
|
+
num_new_props += 1
|
606
|
+
|
607
|
+
# Determine the conversion quality
|
608
|
+
if num_out_props > 0:
|
609
|
+
qual_ratio = 1 - num_new_props/num_out_props
|
610
|
+
else:
|
611
|
+
qual_ratio = 1
|
612
|
+
|
613
|
+
if any_unknown:
|
614
|
+
qual_str = const.QUAL_UNKNOWN
|
615
|
+
elif num_out_props == 0 or qual_ratio >= 0.8:
|
616
|
+
qual_str = const.QUAL_VERYGOOD
|
617
|
+
elif qual_ratio >= 0.6:
|
618
|
+
qual_str = const.QUAL_GOOD
|
619
|
+
elif qual_ratio >= 0.4:
|
620
|
+
qual_str = const.QUAL_OKAY
|
621
|
+
elif qual_ratio >= 0.2:
|
622
|
+
qual_str = const.QUAL_POOR
|
623
|
+
else:
|
624
|
+
qual_str = const.QUAL_VERYPOOR
|
625
|
+
|
626
|
+
# Construct the details string for info on possible issues with the conversion
|
627
|
+
|
628
|
+
# Sort the keys by label alphabetically
|
629
|
+
l_props: list[str] = list(d_prop_conversion_info.keys())
|
630
|
+
l_props.sort(key=lambda x: d_prop_conversion_info[x].label)
|
631
|
+
|
632
|
+
details = "\n".join([d_prop_conversion_info[x].note for x in l_props if d_prop_conversion_info[x].note])
|
633
|
+
|
634
|
+
return ConversionQualityInfo(converter_name=converter_name,
|
635
|
+
in_format=in_format,
|
636
|
+
out_format=out_format,
|
637
|
+
qual_str=qual_str,
|
638
|
+
details=details,
|
639
|
+
d_prop_conversion_info=d_prop_conversion_info)
|
640
|
+
|
641
|
+
def get_possible_converters(self,
|
642
|
+
in_format: str,
|
643
|
+
out_format: str) -> list[str]:
|
644
|
+
"""Get a list of converters which can perform a conversion from one format to another and the degree of success
|
645
|
+
with each of these converters
|
646
|
+
|
647
|
+
Parameters
|
648
|
+
----------
|
649
|
+
in_format : str
|
650
|
+
The extension of the input file format
|
651
|
+
out_format : str
|
652
|
+
The extension of the output file format
|
653
|
+
|
654
|
+
Returns
|
655
|
+
-------
|
656
|
+
list[tuple[str, str]]
|
657
|
+
A list of tuples, where each tuple's first item is the name of a converter which can perform this
|
658
|
+
conversion, and the second item is the degree of success for the conversion
|
659
|
+
"""
|
660
|
+
in_id: int = self.parent.get_format_info(in_format).id
|
661
|
+
out_id: int = self.parent.get_format_info(out_format).id
|
662
|
+
|
663
|
+
# Slice the table to get a list of the success for this conversion for each converter
|
664
|
+
l_converter_success = [x[in_id][out_id] for x in self._table]
|
665
|
+
|
666
|
+
# Filter for possible conversions and get the converter name and degree-of-success string
|
667
|
+
# for each possible conversion
|
668
|
+
l_possible_converters = [self.parent.get_converter_info(converter_id).name
|
669
|
+
for converter_id, possible_flag
|
670
|
+
in enumerate(l_converter_success) if possible_flag > 0]
|
671
|
+
|
672
|
+
return l_possible_converters
|
673
|
+
|
674
|
+
def get_possible_formats(self, converter_name: str) -> tuple[list[str], list[str]]:
|
675
|
+
"""Get a list of input and output formats that a given converter supports
|
676
|
+
|
677
|
+
Parameters
|
678
|
+
----------
|
679
|
+
converter_name : str
|
680
|
+
The name of the converter
|
681
|
+
|
682
|
+
Returns
|
683
|
+
-------
|
684
|
+
tuple[list[str], list[str]]
|
685
|
+
A tuple of a list of the supported input formats and a list of the supported output formats
|
686
|
+
"""
|
687
|
+
conv_id: int = self.parent.get_converter_info(converter_name).id
|
688
|
+
ll_in_out_format_success = self._table[conv_id]
|
689
|
+
|
690
|
+
# Filter for possible input formats by checking if at least one output format for each has a degree of success
|
691
|
+
# index greater than 0, and stored the filtered lists where the input format is possible so we only need to
|
692
|
+
# check them for possible output formats
|
693
|
+
(l_possible_in_format_ids,
|
694
|
+
ll_filtered_in_out_format_success) = zip(*[(i, l_out_format_success) for i, l_out_format_success
|
695
|
+
in enumerate(ll_in_out_format_success)
|
696
|
+
if sum(l_out_format_success) > 0])
|
697
|
+
|
698
|
+
# As with input IDs, filter for output IDs where at least one input format has a degree of success index greater
|
699
|
+
# than 0. A bit more complicated for the second index, forcing us to do list comprehension to fetch a list
|
700
|
+
# across the table before summing
|
701
|
+
l_possible_out_format_ids = [j for j, _ in enumerate(ll_filtered_in_out_format_success[0]) if
|
702
|
+
sum([x[j] for x in ll_filtered_in_out_format_success]) > 0]
|
703
|
+
|
704
|
+
# Get the name for each format ID, and return lists of the names
|
705
|
+
return ([self.parent.get_format_info(x).name for x in l_possible_in_format_ids],
|
706
|
+
[self.parent.get_format_info(x).name for x in l_possible_out_format_ids])
|
707
|
+
|
708
|
+
|
709
|
+
class DataConversionDatabase:
|
710
|
+
"""Class providing interface for information contained in the PSDI Data Conversion database
|
711
|
+
"""
|
712
|
+
|
713
|
+
def __init__(self, d_data: dict[str, Any]):
|
714
|
+
"""Initialise the DataConversionDatabase object
|
715
|
+
|
716
|
+
Parameters
|
717
|
+
----------
|
718
|
+
d_data : dict[str, Any]
|
719
|
+
The dict of the database, as loaded in from the JSON file
|
720
|
+
"""
|
721
|
+
|
722
|
+
# Store the database dict internally for debugging purposes
|
723
|
+
self._d_data = d_data
|
724
|
+
|
725
|
+
# Store top-level items not tied to a specific converter
|
726
|
+
self.formats: list[dict[str, bool | int | str | None]] = d_data[DB_FORMATS_KEY]
|
727
|
+
self.converters: list[dict[str, bool | int | str | None]] = d_data[DB_CONVERTERS_KEY]
|
728
|
+
self.converts_to: list[dict[str, bool | int | str | None]] = d_data[DB_CONVERTS_TO_KEY]
|
729
|
+
|
730
|
+
# Placeholders for properties that are generated when needed
|
731
|
+
self._d_converter_info: dict[str, ConverterInfo] | None = None
|
732
|
+
self._l_converter_info: list[ConverterInfo] | None = None
|
733
|
+
self._d_format_info: dict[str, FormatInfo] | None = None
|
734
|
+
self._l_format_info: list[FormatInfo] | None = None
|
735
|
+
self._conversions_table: ConversionsTable | None = None
|
736
|
+
|
737
|
+
@property
|
738
|
+
def d_converter_info(self) -> dict[str, ConverterInfo]:
|
739
|
+
"""Generate the converter info dict (indexed by name) when needed
|
740
|
+
"""
|
741
|
+
if self._d_converter_info is None:
|
742
|
+
self._d_converter_info: dict[str, ConverterInfo] = {}
|
743
|
+
for d_single_converter_info in self.converters:
|
744
|
+
name: str = d_single_converter_info[DB_NAME_KEY]
|
745
|
+
if name in self._d_converter_info:
|
746
|
+
logger.warning(f"Converter '{name}' appears more than once in the database. Only the first instance"
|
747
|
+
" will be used.")
|
748
|
+
continue
|
749
|
+
|
750
|
+
self._d_converter_info[name] = ConverterInfo(name=name,
|
751
|
+
parent=self,
|
752
|
+
d_single_converter_info=d_single_converter_info,
|
753
|
+
d_data=self._d_data)
|
754
|
+
return self._d_converter_info
|
755
|
+
|
756
|
+
@property
|
757
|
+
def l_converter_info(self) -> list[ConverterInfo | None]:
|
758
|
+
"""Generate the converter info list (indexed by ID) when needed
|
759
|
+
"""
|
760
|
+
if self._l_converter_info is None:
|
761
|
+
# Pre-size a list based on the maximum ID plus 1 (since IDs are 1-indexed)
|
762
|
+
max_id: int = max([x[DB_ID_KEY] for x in self.converters])
|
763
|
+
self._l_converter_info: list[ConverterInfo | None] = [None] * (max_id+1)
|
764
|
+
|
765
|
+
# Fill the list with all converters in the dict
|
766
|
+
for single_converter_info in self.d_converter_info.values():
|
767
|
+
self._l_converter_info[single_converter_info.id] = single_converter_info
|
768
|
+
|
769
|
+
return self._l_converter_info
|
770
|
+
|
771
|
+
@property
|
772
|
+
def d_format_info(self) -> dict[str, FormatInfo]:
|
773
|
+
"""Generate the format info dict when needed
|
774
|
+
"""
|
775
|
+
if self._d_format_info is None:
|
776
|
+
self._d_format_info: dict[str, FormatInfo] = {}
|
777
|
+
|
778
|
+
for d_single_format_info in self.formats:
|
779
|
+
name: str = d_single_format_info[DB_FORMAT_EXT_KEY]
|
780
|
+
|
781
|
+
format_info = FormatInfo(name=name,
|
782
|
+
parent=self,
|
783
|
+
d_single_format_info=d_single_format_info)
|
784
|
+
|
785
|
+
if name in self._d_format_info:
|
786
|
+
logger.debug(f"File extension '{name}' appears more than once in the database. Duplicates will use "
|
787
|
+
"a key appended with an index")
|
788
|
+
loop_concluded = False
|
789
|
+
for i in range(97):
|
790
|
+
test_name = f"{name}-{i+2}"
|
791
|
+
if test_name in self._d_format_info:
|
792
|
+
continue
|
793
|
+
else:
|
794
|
+
self._d_format_info[test_name] = format_info
|
795
|
+
loop_concluded = True
|
796
|
+
break
|
797
|
+
if not loop_concluded:
|
798
|
+
logger.warning("Loop counter exceeded when searching for valid new name for file extension "
|
799
|
+
f"'{name}'. New entry will not be added to the database to avoid possibility of "
|
800
|
+
"an infinite loop")
|
801
|
+
else:
|
802
|
+
self._d_format_info[name] = format_info
|
803
|
+
return self._d_format_info
|
804
|
+
|
805
|
+
@property
|
806
|
+
def l_format_info(self) -> list[FormatInfo | None]:
|
807
|
+
"""Generate the format info list (indexed by ID) when needed
|
808
|
+
"""
|
809
|
+
if self._l_format_info is None:
|
810
|
+
# Pre-size a list based on the maximum ID plus 1 (since IDs are 1-indexed)
|
811
|
+
max_id: int = max([x[DB_ID_KEY] for x in self.formats])
|
812
|
+
self._l_format_info: list[FormatInfo | None] = [None] * (max_id+1)
|
813
|
+
|
814
|
+
# Fill the list with all formats in the dict
|
815
|
+
for single_format_info in self.d_format_info.values():
|
816
|
+
self._l_format_info[single_format_info.id] = single_format_info
|
817
|
+
|
818
|
+
return self._l_format_info
|
819
|
+
|
820
|
+
@property
|
821
|
+
def conversions_table(self) -> ConversionsTable:
|
822
|
+
"""Generates the conversions table when needed
|
823
|
+
"""
|
824
|
+
if self._conversions_table is None:
|
825
|
+
self._conversions_table = ConversionsTable(l_converts_to=self.converts_to,
|
826
|
+
parent=self)
|
827
|
+
return self._conversions_table
|
828
|
+
|
829
|
+
def get_converter_info(self, converter_name_or_id: str | int) -> ConverterInfo:
|
830
|
+
"""Get a converter's info from either its name or ID
|
831
|
+
"""
|
832
|
+
if isinstance(converter_name_or_id, str):
|
833
|
+
try:
|
834
|
+
return self.d_converter_info[converter_name_or_id]
|
835
|
+
except KeyError:
|
836
|
+
raise FileConverterDatabaseException(f"Converter name '{converter_name_or_id}' not recognised")
|
837
|
+
elif isinstance(converter_name_or_id, int):
|
838
|
+
return self.l_converter_info[converter_name_or_id]
|
839
|
+
else:
|
840
|
+
raise FileConverterDatabaseException(f"Invalid key passed to `get_converter_info`: '{converter_name_or_id}'"
|
841
|
+
f" of type '{type(converter_name_or_id)}'. Type must be `str` or "
|
842
|
+
"`int`")
|
843
|
+
|
844
|
+
def get_format_info(self, format_name_or_id: str | int) -> FormatInfo:
|
845
|
+
"""Get a format's ID info from either its name or ID
|
846
|
+
"""
|
847
|
+
if isinstance(format_name_or_id, str):
|
848
|
+
try:
|
849
|
+
return self.d_format_info[format_name_or_id]
|
850
|
+
except KeyError:
|
851
|
+
raise FileConverterDatabaseException(f"Format name '{format_name_or_id}' not recognised")
|
852
|
+
elif isinstance(format_name_or_id, int):
|
853
|
+
return self.l_format_info[format_name_or_id]
|
854
|
+
else:
|
855
|
+
raise FileConverterDatabaseException(f"Invalid key passed to `get_format_info`: '{format_name_or_id}'"
|
856
|
+
f" of type '{type(format_name_or_id)}'. Type must be `str` or "
|
857
|
+
"`int`")
|
858
|
+
|
859
|
+
|
860
|
+
# The database will be loaded on demand when `get_database()` is called
|
861
|
+
_database: DataConversionDatabase | None = None
|
862
|
+
|
863
|
+
|
864
|
+
def load_database() -> DataConversionDatabase:
|
865
|
+
"""Load and return a new instance of the data conversion database from the JSON database file in this package. This
|
866
|
+
function should not be called directly unless you specifically need a new instance of the database object and can't
|
867
|
+
deepcopy the database returned by `get_database()`, as it's expensive to load it in.
|
868
|
+
|
869
|
+
Returns
|
870
|
+
-------
|
871
|
+
DataConversionDatabase
|
872
|
+
"""
|
873
|
+
|
874
|
+
# Find and load the database JSON file
|
875
|
+
|
876
|
+
# For an interactive shell, __file__ won't be defined for this module, so use the constants module instead
|
877
|
+
reference_file = os.path.realpath(const.__file__)
|
878
|
+
|
879
|
+
qualified_database_filename = os.path.join(os.path.dirname(reference_file), const.DATABASE_FILENAME)
|
880
|
+
d_data: dict = json.load(open(qualified_database_filename, "r"))
|
881
|
+
|
882
|
+
return DataConversionDatabase(d_data)
|
883
|
+
|
884
|
+
|
885
|
+
def get_database() -> DataConversionDatabase:
|
886
|
+
"""Gets the global database object, loading it in first if necessary. Since it's computationally expensive to load
|
887
|
+
the database, it's best treated as an immutable singleton.
|
888
|
+
|
889
|
+
Returns
|
890
|
+
-------
|
891
|
+
DataConversionDatabase
|
892
|
+
The global database object
|
893
|
+
"""
|
894
|
+
global _database
|
895
|
+
if _database is None:
|
896
|
+
# Create the database object and store it globally
|
897
|
+
_database = load_database()
|
898
|
+
return _database
|
899
|
+
|
900
|
+
|
901
|
+
def get_converter_info(name: str) -> ConverterInfo:
|
902
|
+
"""Gets the information on a given converter stored in the database
|
903
|
+
|
904
|
+
Parameters
|
905
|
+
----------
|
906
|
+
name : str
|
907
|
+
The name of the converter
|
908
|
+
|
909
|
+
Returns
|
910
|
+
-------
|
911
|
+
ConverterInfo
|
912
|
+
"""
|
913
|
+
|
914
|
+
return get_database().d_converter_info[name]
|
915
|
+
|
916
|
+
|
917
|
+
def get_format_info(name: str) -> FormatInfo:
|
918
|
+
"""Gets the information on a given file format stored in the database
|
919
|
+
|
920
|
+
Parameters
|
921
|
+
----------
|
922
|
+
name : str
|
923
|
+
The name (extension) of the form
|
924
|
+
|
925
|
+
Returns
|
926
|
+
-------
|
927
|
+
FormatInfo
|
928
|
+
"""
|
929
|
+
|
930
|
+
return get_database().d_format_info[name]
|
931
|
+
|
932
|
+
|
933
|
+
def get_conversion_quality(converter_name: str,
|
934
|
+
in_format: str,
|
935
|
+
out_format: str) -> ConversionQualityInfo | None:
|
936
|
+
"""Get an indication of the quality of a conversion from one format to another, or if it's not possible
|
937
|
+
|
938
|
+
Parameters
|
939
|
+
----------
|
940
|
+
converter_name : str
|
941
|
+
The name of the converter
|
942
|
+
in_format : str
|
943
|
+
The extension of the input file format
|
944
|
+
out_format : str
|
945
|
+
The extension of the output file format
|
946
|
+
|
947
|
+
Returns
|
948
|
+
-------
|
949
|
+
ConversionQualityInfo | None
|
950
|
+
If the conversion is not possible, returns None. If the conversion is possible, returns a
|
951
|
+
`ConversionQualityInfo` object with info on the conversion
|
952
|
+
"""
|
953
|
+
|
954
|
+
return get_database().conversions_table.get_conversion_quality(converter_name=converter_name,
|
955
|
+
in_format=in_format,
|
956
|
+
out_format=out_format)
|
957
|
+
|
958
|
+
|
959
|
+
def get_possible_converters(in_format: str,
|
960
|
+
out_format: str) -> list[str]:
|
961
|
+
"""Get a list of converters which can perform a conversion from one format to another and the degree of success
|
962
|
+
with each of these converters
|
963
|
+
|
964
|
+
Parameters
|
965
|
+
----------
|
966
|
+
in_format : str
|
967
|
+
The extension of the input file format
|
968
|
+
out_format : str
|
969
|
+
The extension of the output file format
|
970
|
+
|
971
|
+
Returns
|
972
|
+
-------
|
973
|
+
list[tuple[str, str]]
|
974
|
+
A list of tuples, where each tuple's first item is the name of a converter which can perform this
|
975
|
+
conversion, and the second item is the degree of success for the conversion
|
976
|
+
"""
|
977
|
+
|
978
|
+
return get_database().conversions_table.get_possible_converters(in_format=in_format,
|
979
|
+
out_format=out_format)
|
980
|
+
|
981
|
+
|
982
|
+
def get_possible_formats(converter_name: str) -> tuple[list[str], list[str]]:
|
983
|
+
"""Get a list of input and output formats that a given converter supports
|
984
|
+
|
985
|
+
Parameters
|
986
|
+
----------
|
987
|
+
converter_name : str
|
988
|
+
The name of the converter
|
989
|
+
|
990
|
+
Returns
|
991
|
+
-------
|
992
|
+
tuple[list[str], list[str]]
|
993
|
+
A tuple of a list of the supported input formats and a list of the supported output formats
|
994
|
+
"""
|
995
|
+
return get_database().conversions_table.get_possible_formats(converter_name=converter_name)
|
996
|
+
|
997
|
+
|
998
|
+
def _find_arg(tl_args: tuple[list[FlagInfo], list[OptionInfo]],
|
999
|
+
arg: str) -> ArgInfo:
|
1000
|
+
"""Find a specific flag or option in the lists
|
1001
|
+
"""
|
1002
|
+
for l_args in tl_args:
|
1003
|
+
l_found = [x for x in l_args if x.flag == arg]
|
1004
|
+
if len(l_found) > 0:
|
1005
|
+
return l_found[0]
|
1006
|
+
# If we get here, it wasn't found in either list
|
1007
|
+
raise FileConverterDatabaseException(f"Argument {arg} was not found in the list of allowed arguments for this "
|
1008
|
+
"conversion")
|
1009
|
+
|
1010
|
+
|
1011
|
+
def get_in_format_args(converter_name: str,
|
1012
|
+
format_name: str,
|
1013
|
+
arg: str | None = None) -> tuple[list[FlagInfo], list[OptionInfo]] | ArgInfo:
|
1014
|
+
"""Get the input flags and options supported by a given converter for a given format (provided as its extension).
|
1015
|
+
Optionally will provide information on just a single flag or option if its value is provided as an optional argument
|
1016
|
+
|
1017
|
+
Parameters
|
1018
|
+
----------
|
1019
|
+
converter_name : str
|
1020
|
+
The converter name
|
1021
|
+
format_name : str
|
1022
|
+
The file format name (extension)
|
1023
|
+
arg : str | None
|
1024
|
+
If provided, only information on this flag or option will be provided
|
1025
|
+
|
1026
|
+
Returns
|
1027
|
+
-------
|
1028
|
+
tuple[set[FlagInfo], set[OptionInfo]]
|
1029
|
+
A list of info for the allowed flags, and a set of info for the allowed options
|
1030
|
+
"""
|
1031
|
+
|
1032
|
+
converter_info = get_converter_info(converter_name)
|
1033
|
+
tl_args = converter_info.get_in_format_args(format_name)
|
1034
|
+
if not arg:
|
1035
|
+
return tl_args
|
1036
|
+
return _find_arg(tl_args, arg)
|
1037
|
+
|
1038
|
+
|
1039
|
+
def get_out_format_args(converter_name: str,
|
1040
|
+
format_name: str,
|
1041
|
+
arg: str | None = None) -> tuple[list[FlagInfo], list[OptionInfo]]:
|
1042
|
+
"""Get the output flags and options supported by a given converter for a given format (provided as its extension).
|
1043
|
+
Optionally will provide information on just a single flag or option if its value is provided as an optional argument
|
1044
|
+
|
1045
|
+
Parameters
|
1046
|
+
----------
|
1047
|
+
converter_name : str
|
1048
|
+
The converter name
|
1049
|
+
format_name : str
|
1050
|
+
The file format name (extension)
|
1051
|
+
arg : str | None
|
1052
|
+
If provided, only information on this flag or option will be provided
|
1053
|
+
|
1054
|
+
Returns
|
1055
|
+
-------
|
1056
|
+
tuple[set[FlagInfo], set[OptionInfo]]
|
1057
|
+
A list of info for the allowed flags, and a set of info for the allowed options
|
1058
|
+
"""
|
1059
|
+
|
1060
|
+
converter_info = get_converter_info(converter_name)
|
1061
|
+
tl_args = converter_info.get_out_format_args(format_name)
|
1062
|
+
if not arg:
|
1063
|
+
return tl_args
|
1064
|
+
return _find_arg(tl_args, arg)
|