psdi-data-conversion 0.0.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. psdi_data_conversion/__init__.py +11 -0
  2. psdi_data_conversion/app.py +242 -0
  3. psdi_data_conversion/bin/linux/atomsk +0 -0
  4. psdi_data_conversion/bin/linux/c2x +0 -0
  5. psdi_data_conversion/bin/mac/atomsk +0 -0
  6. psdi_data_conversion/bin/mac/c2x +0 -0
  7. psdi_data_conversion/constants.py +185 -0
  8. psdi_data_conversion/converter.py +459 -0
  9. psdi_data_conversion/converters/__init__.py +6 -0
  10. psdi_data_conversion/converters/atomsk.py +32 -0
  11. psdi_data_conversion/converters/base.py +702 -0
  12. psdi_data_conversion/converters/c2x.py +32 -0
  13. psdi_data_conversion/converters/openbabel.py +239 -0
  14. psdi_data_conversion/database.py +1064 -0
  15. psdi_data_conversion/dist.py +87 -0
  16. psdi_data_conversion/file_io.py +216 -0
  17. psdi_data_conversion/log_utility.py +241 -0
  18. psdi_data_conversion/main.py +776 -0
  19. psdi_data_conversion/scripts/atomsk.sh +32 -0
  20. psdi_data_conversion/scripts/c2x.sh +26 -0
  21. psdi_data_conversion/security.py +38 -0
  22. psdi_data_conversion/static/content/accessibility.htm +254 -0
  23. psdi_data_conversion/static/content/convert.htm +121 -0
  24. psdi_data_conversion/static/content/convertato.htm +65 -0
  25. psdi_data_conversion/static/content/convertc2x.htm +65 -0
  26. psdi_data_conversion/static/content/documentation.htm +94 -0
  27. psdi_data_conversion/static/content/feedback.htm +53 -0
  28. psdi_data_conversion/static/content/header-links.html +8 -0
  29. psdi_data_conversion/static/content/index-versions/header-links.html +8 -0
  30. psdi_data_conversion/static/content/index-versions/psdi-common-footer.html +99 -0
  31. psdi_data_conversion/static/content/index-versions/psdi-common-header.html +28 -0
  32. psdi_data_conversion/static/content/psdi-common-footer.html +99 -0
  33. psdi_data_conversion/static/content/psdi-common-header.html +28 -0
  34. psdi_data_conversion/static/content/report.htm +103 -0
  35. psdi_data_conversion/static/data/data.json +143940 -0
  36. psdi_data_conversion/static/img/colormode-toggle-dm.svg +3 -0
  37. psdi_data_conversion/static/img/colormode-toggle-lm.svg +3 -0
  38. psdi_data_conversion/static/img/psdi-icon-dark.svg +136 -0
  39. psdi_data_conversion/static/img/psdi-icon-light.svg +208 -0
  40. psdi_data_conversion/static/img/psdi-logo-darktext.png +0 -0
  41. psdi_data_conversion/static/img/psdi-logo-lighttext.png +0 -0
  42. psdi_data_conversion/static/img/social-logo-bluesky-black.svg +4 -0
  43. psdi_data_conversion/static/img/social-logo-bluesky-white.svg +4 -0
  44. psdi_data_conversion/static/img/social-logo-instagram-black.svg +1 -0
  45. psdi_data_conversion/static/img/social-logo-instagram-white.svg +1 -0
  46. psdi_data_conversion/static/img/social-logo-linkedin-black.png +0 -0
  47. psdi_data_conversion/static/img/social-logo-linkedin-white.png +0 -0
  48. psdi_data_conversion/static/img/social-logo-mastodon-black.svg +4 -0
  49. psdi_data_conversion/static/img/social-logo-mastodon-white.svg +4 -0
  50. psdi_data_conversion/static/img/social-logo-x-black.svg +3 -0
  51. psdi_data_conversion/static/img/social-logo-x-white.svg +3 -0
  52. psdi_data_conversion/static/img/social-logo-youtube-black.png +0 -0
  53. psdi_data_conversion/static/img/social-logo-youtube-white.png +0 -0
  54. psdi_data_conversion/static/img/ukri-epsr-logo-darktext.png +0 -0
  55. psdi_data_conversion/static/img/ukri-epsr-logo-lighttext.png +0 -0
  56. psdi_data_conversion/static/img/ukri-logo-darktext.png +0 -0
  57. psdi_data_conversion/static/img/ukri-logo-lighttext.png +0 -0
  58. psdi_data_conversion/static/javascript/accessibility.js +196 -0
  59. psdi_data_conversion/static/javascript/common.js +42 -0
  60. psdi_data_conversion/static/javascript/convert.js +296 -0
  61. psdi_data_conversion/static/javascript/convert_common.js +252 -0
  62. psdi_data_conversion/static/javascript/convertato.js +107 -0
  63. psdi_data_conversion/static/javascript/convertc2x.js +107 -0
  64. psdi_data_conversion/static/javascript/data.js +176 -0
  65. psdi_data_conversion/static/javascript/format.js +611 -0
  66. psdi_data_conversion/static/javascript/load_accessibility.js +89 -0
  67. psdi_data_conversion/static/javascript/psdi-common.js +177 -0
  68. psdi_data_conversion/static/javascript/report.js +381 -0
  69. psdi_data_conversion/static/styles/format.css +147 -0
  70. psdi_data_conversion/static/styles/psdi-common.css +705 -0
  71. psdi_data_conversion/templates/index.htm +114 -0
  72. psdi_data_conversion/testing/__init__.py +5 -0
  73. psdi_data_conversion/testing/constants.py +12 -0
  74. psdi_data_conversion/testing/conversion_callbacks.py +394 -0
  75. psdi_data_conversion/testing/conversion_test_specs.py +208 -0
  76. psdi_data_conversion/testing/utils.py +522 -0
  77. psdi_data_conversion-0.0.23.dist-info/METADATA +663 -0
  78. psdi_data_conversion-0.0.23.dist-info/RECORD +81 -0
  79. psdi_data_conversion-0.0.23.dist-info/WHEEL +4 -0
  80. psdi_data_conversion-0.0.23.dist-info/entry_points.txt +2 -0
  81. psdi_data_conversion-0.0.23.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,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)