psdi-data-conversion 0.0.38__py3-none-any.whl → 0.1.0__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/app.py +93 -33
- psdi_data_conversion/constants.py +1 -0
- psdi_data_conversion/converter.py +145 -17
- psdi_data_conversion/converters/base.py +24 -20
- psdi_data_conversion/converters/c2x.py +13 -0
- psdi_data_conversion/converters/openbabel.py +2 -1
- psdi_data_conversion/database.py +46 -14
- psdi_data_conversion/dist.py +2 -1
- psdi_data_conversion/file_io.py +1 -2
- psdi_data_conversion/log_utility.py +1 -1
- psdi_data_conversion/main.py +32 -25
- psdi_data_conversion/static/content/index-versions/psdi-common-footer.html +13 -9
- psdi_data_conversion/static/content/index-versions/psdi-common-header.html +1 -1
- psdi_data_conversion/static/content/psdi-common-footer.html +13 -9
- psdi_data_conversion/static/content/psdi-common-header.html +1 -1
- psdi_data_conversion/static/data/data.json +617 -3
- psdi_data_conversion/static/javascript/convert.js +54 -6
- psdi_data_conversion/static/javascript/convert_common.js +16 -2
- psdi_data_conversion/static/javascript/data.js +18 -0
- psdi_data_conversion/static/styles/format.css +7 -0
- psdi_data_conversion/templates/index.htm +8 -9
- psdi_data_conversion/testing/conversion_callbacks.py +2 -2
- psdi_data_conversion/testing/conversion_test_specs.py +27 -7
- psdi_data_conversion/testing/gui.py +18 -12
- psdi_data_conversion/testing/utils.py +3 -3
- psdi_data_conversion/utils.py +21 -0
- {psdi_data_conversion-0.0.38.dist-info → psdi_data_conversion-0.1.0.dist-info}/METADATA +2 -2
- {psdi_data_conversion-0.0.38.dist-info → psdi_data_conversion-0.1.0.dist-info}/RECORD +31 -30
- {psdi_data_conversion-0.0.38.dist-info → psdi_data_conversion-0.1.0.dist-info}/WHEEL +0 -0
- {psdi_data_conversion-0.0.38.dist-info → psdi_data_conversion-0.1.0.dist-info}/entry_points.txt +0 -0
- {psdi_data_conversion-0.0.38.dist-info → psdi_data_conversion-0.1.0.dist-info}/licenses/LICENSE +0 -0
psdi_data_conversion/database.py
CHANGED
@@ -7,16 +7,17 @@ Python module provide utilities for accessing the converter database
|
|
7
7
|
|
8
8
|
from __future__ import annotations
|
9
9
|
|
10
|
+
import json
|
11
|
+
import os
|
10
12
|
from dataclasses import dataclass, field
|
11
13
|
from itertools import product
|
12
|
-
import json
|
13
14
|
from logging import getLogger
|
14
|
-
import
|
15
|
-
from typing import Any, Literal
|
15
|
+
from typing import Any, Literal, overload
|
16
16
|
|
17
17
|
from psdi_data_conversion import constants as const
|
18
|
-
from psdi_data_conversion.converter import
|
18
|
+
from psdi_data_conversion.converter import D_SUPPORTED_CONVERTERS, get_registered_converter_class
|
19
19
|
from psdi_data_conversion.converters.base import FileConverterException
|
20
|
+
from psdi_data_conversion.utils import regularize_name
|
20
21
|
|
21
22
|
# Keys for top-level and general items in the database
|
22
23
|
DB_FORMATS_KEY = "formats"
|
@@ -32,6 +33,7 @@ DB_URL_KEY = "url"
|
|
32
33
|
|
33
34
|
# Keys for format general info in the database
|
34
35
|
DB_FORMAT_EXT_KEY = "extension"
|
36
|
+
DB_FORMAT_C2X_KEY = "format"
|
35
37
|
DB_FORMAT_NOTE_KEY = "note"
|
36
38
|
DB_FORMAT_COMP_KEY = "composition"
|
37
39
|
DB_FORMAT_CONN_KEY = "connections"
|
@@ -116,14 +118,14 @@ class ConverterInfo:
|
|
116
118
|
Parameters
|
117
119
|
----------
|
118
120
|
name : str
|
119
|
-
The name of the converter
|
121
|
+
The regularized name of the converter
|
120
122
|
parent : DataConversionDatabase
|
121
123
|
The database which this belongs to
|
122
124
|
d_data : dict[str, Any]
|
123
125
|
The loaded database dict
|
124
126
|
"""
|
125
127
|
|
126
|
-
self.name = name
|
128
|
+
self.name = regularize_name(name)
|
127
129
|
self.parent = parent
|
128
130
|
|
129
131
|
# Get info about the converter from the database
|
@@ -133,7 +135,7 @@ class ConverterInfo:
|
|
133
135
|
|
134
136
|
# Get necessary info about the converter from the class
|
135
137
|
try:
|
136
|
-
self._key_prefix =
|
138
|
+
self._key_prefix = get_registered_converter_class(name).database_key_prefix
|
137
139
|
except KeyError:
|
138
140
|
# We'll get a KeyError for converters in the database that don't yet have their own class, which we can
|
139
141
|
# safely ignore
|
@@ -428,6 +430,9 @@ class FormatInfo:
|
|
428
430
|
self.id: int = d_single_format_info.get(DB_ID_KEY, -1)
|
429
431
|
"""The ID of this format"""
|
430
432
|
|
433
|
+
self.c2x_format: str = d_single_format_info.get(DB_FORMAT_C2X_KEY)
|
434
|
+
"""The name of this format as the c2x converter expects it"""
|
435
|
+
|
431
436
|
self.note: str = d_single_format_info.get(DB_FORMAT_NOTE_KEY, "")
|
432
437
|
"""The description of this format"""
|
433
438
|
|
@@ -525,6 +530,10 @@ class ConversionQualityInfo:
|
|
525
530
|
input and output file formats and a note on the implications
|
526
531
|
"""
|
527
532
|
|
533
|
+
def __post_init__(self):
|
534
|
+
"""Regularize the converter name"""
|
535
|
+
self.converter_name = regularize_name(self.converter_name)
|
536
|
+
|
528
537
|
|
529
538
|
class ConversionsTable:
|
530
539
|
"""Class providing information on available file format conversions.
|
@@ -619,7 +628,7 @@ class ConversionsTable:
|
|
619
628
|
|
620
629
|
# Check if this converter deals with ambiguous formats, so we know if we need to be strict about getting format
|
621
630
|
# info
|
622
|
-
if
|
631
|
+
if get_registered_converter_class(converter_name).supports_ambiguous_extensions:
|
623
632
|
which_format = None
|
624
633
|
else:
|
625
634
|
which_format = 0
|
@@ -708,8 +717,8 @@ class ConversionsTable:
|
|
708
717
|
conversion, the second is the info of the input format for this conversion, and the third is the info of the
|
709
718
|
output format
|
710
719
|
"""
|
711
|
-
l_in_format_infos
|
712
|
-
l_out_format_infos
|
720
|
+
l_in_format_infos = self.parent.get_format_info(in_format, which="all")
|
721
|
+
l_out_format_infos = self.parent.get_format_info(out_format, which="all")
|
713
722
|
|
714
723
|
# Start a list of all possible conversions
|
715
724
|
l_possible_conversions = []
|
@@ -801,7 +810,7 @@ class DataConversionDatabase:
|
|
801
810
|
if self._d_converter_info is None:
|
802
811
|
self._d_converter_info: dict[str, ConverterInfo] = {}
|
803
812
|
for d_single_converter_info in self.converters:
|
804
|
-
name: str = d_single_converter_info[DB_NAME_KEY]
|
813
|
+
name: str = regularize_name(d_single_converter_info[DB_NAME_KEY])
|
805
814
|
if name in self._d_converter_info:
|
806
815
|
logger.warning(f"Converter '{name}' appears more than once in the database. Only the first instance"
|
807
816
|
" will be used.")
|
@@ -934,6 +943,16 @@ class DataConversionDatabase:
|
|
934
943
|
f" of type '{type(converter_name_or_id)}'. Type must be `str` or "
|
935
944
|
"`int`")
|
936
945
|
|
946
|
+
@overload
|
947
|
+
def get_format_info(self,
|
948
|
+
format_name_or_id: str | int | FormatInfo,
|
949
|
+
which: int | None = None) -> FormatInfo: ...
|
950
|
+
|
951
|
+
@overload
|
952
|
+
def get_format_info(self,
|
953
|
+
format_name_or_id: str | int | FormatInfo,
|
954
|
+
which: Literal["all"]) -> list[FormatInfo]: ...
|
955
|
+
|
937
956
|
def get_format_info(self,
|
938
957
|
format_name_or_id: str | int | FormatInfo,
|
939
958
|
which: int | Literal["all"] | None = None) -> FormatInfo | list[FormatInfo]:
|
@@ -1081,7 +1100,17 @@ def get_converter_info(name: str) -> ConverterInfo:
|
|
1081
1100
|
ConverterInfo
|
1082
1101
|
"""
|
1083
1102
|
|
1084
|
-
return get_database().d_converter_info[name]
|
1103
|
+
return get_database().d_converter_info[regularize_name(name)]
|
1104
|
+
|
1105
|
+
|
1106
|
+
@overload
|
1107
|
+
def get_format_info(format_name_or_id: str | int | FormatInfo,
|
1108
|
+
which: int | None = None) -> FormatInfo: ...
|
1109
|
+
|
1110
|
+
|
1111
|
+
@overload
|
1112
|
+
def get_format_info(format_name_or_id: str | int | FormatInfo,
|
1113
|
+
which: Literal["all"]) -> list[FormatInfo]: ...
|
1085
1114
|
|
1086
1115
|
|
1087
1116
|
def get_format_info(format_name_or_id: str | int | FormatInfo,
|
@@ -1131,7 +1160,7 @@ def get_conversion_quality(converter_name: str,
|
|
1131
1160
|
`ConversionQualityInfo` object with info on the conversion
|
1132
1161
|
"""
|
1133
1162
|
|
1134
|
-
return get_database().conversions_table.get_conversion_quality(converter_name=converter_name,
|
1163
|
+
return get_database().conversions_table.get_conversion_quality(converter_name=regularize_name(converter_name),
|
1135
1164
|
in_format=in_format,
|
1136
1165
|
out_format=out_format)
|
1137
1166
|
|
@@ -1186,6 +1215,9 @@ def disambiguate_formats(converter_name: str,
|
|
1186
1215
|
If more than one format combination is possible for this conversion, or no conversion is possible
|
1187
1216
|
"""
|
1188
1217
|
|
1218
|
+
# Regularize the converter name so we don't worry about case/spacing mismatches
|
1219
|
+
converter_name = regularize_name(converter_name)
|
1220
|
+
|
1189
1221
|
# Get all possible conversions, and see if we only have one for this converter
|
1190
1222
|
l_possible_conversions = [x for x in get_possible_conversions(in_format, out_format)
|
1191
1223
|
if x[0] == converter_name]
|
@@ -1219,7 +1251,7 @@ def get_possible_formats(converter_name: str) -> tuple[list[FormatInfo], list[Fo
|
|
1219
1251
|
tuple[list[FormatInfo], list[FormatInfo]]
|
1220
1252
|
A tuple of a list of the supported input formats and a list of the supported output formats
|
1221
1253
|
"""
|
1222
|
-
return get_database().conversions_table.get_possible_formats(converter_name=converter_name)
|
1254
|
+
return get_database().conversions_table.get_possible_formats(converter_name=regularize_name(converter_name))
|
1223
1255
|
|
1224
1256
|
|
1225
1257
|
def _find_arg(tl_args: tuple[list[FlagInfo], list[OptionInfo]],
|
psdi_data_conversion/dist.py
CHANGED
@@ -7,9 +7,10 @@ Functions and utilities related to handling multiple user OSes and distributions
|
|
7
7
|
|
8
8
|
import os
|
9
9
|
import shutil
|
10
|
-
import psdi_data_conversion
|
11
10
|
import sys
|
12
11
|
|
12
|
+
import psdi_data_conversion
|
13
|
+
|
13
14
|
# Labels for each platform (which we use for the folder in this project), and the head of the name each platform will
|
14
15
|
# have in `sys.platform`
|
15
16
|
|
psdi_data_conversion/file_io.py
CHANGED
@@ -149,8 +149,7 @@ def pack_zip_or_tar(archive_filename: str,
|
|
149
149
|
archive_format = _format
|
150
150
|
archive_root_filename = split_archive_ext(archive_filename)[0]
|
151
151
|
break
|
152
|
-
|
153
|
-
if archive_format is None:
|
152
|
+
else:
|
154
153
|
raise AssertionError("Invalid execution path entered - filename wasn't found with a valid archive "
|
155
154
|
"extension, but it did pass the `is_supported_archive` check")
|
156
155
|
else:
|
@@ -5,11 +5,11 @@ Created 2024-12-09 by Bryan Gillis.
|
|
5
5
|
Functions and classes related to logging and other messaging for the user
|
6
6
|
"""
|
7
7
|
|
8
|
-
from datetime import datetime
|
9
8
|
import logging
|
10
9
|
import os
|
11
10
|
import re
|
12
11
|
import sys
|
12
|
+
from datetime import datetime
|
13
13
|
|
14
14
|
from psdi_data_conversion import constants as const
|
15
15
|
|
psdi_data_conversion/main.py
CHANGED
@@ -7,17 +7,19 @@ Created 2025-01-14 by Bryan Gillis.
|
|
7
7
|
Entry-point file for the command-line interface for data conversion.
|
8
8
|
"""
|
9
9
|
|
10
|
-
from itertools import product
|
11
10
|
import logging
|
12
|
-
from argparse import ArgumentParser
|
13
11
|
import os
|
14
12
|
import sys
|
15
13
|
import textwrap
|
14
|
+
from argparse import ArgumentParser
|
15
|
+
from itertools import product
|
16
16
|
|
17
17
|
from psdi_data_conversion import constants as const
|
18
18
|
from psdi_data_conversion.constants import CL_SCRIPT_NAME, CONVERTER_DEFAULT, TERM_WIDTH
|
19
|
-
from psdi_data_conversion.converter import (D_CONVERTER_ARGS,
|
20
|
-
|
19
|
+
from psdi_data_conversion.converter import (D_CONVERTER_ARGS, L_REGISTERED_CONVERTERS, L_SUPPORTED_CONVERTERS,
|
20
|
+
converter_is_registered, converter_is_supported,
|
21
|
+
get_registered_converter_class, get_supported_converter_class,
|
22
|
+
run_converter)
|
21
23
|
from psdi_data_conversion.converters.base import (FileConverterAbortException, FileConverterException,
|
22
24
|
FileConverterInputException)
|
23
25
|
from psdi_data_conversion.database import (FormatInfo, get_conversion_quality, get_converter_info, get_format_info,
|
@@ -25,6 +27,7 @@ from psdi_data_conversion.database import (FormatInfo, get_conversion_quality, g
|
|
25
27
|
get_possible_formats)
|
26
28
|
from psdi_data_conversion.file_io import split_archive_ext
|
27
29
|
from psdi_data_conversion.log_utility import get_log_level_from_str
|
30
|
+
from psdi_data_conversion.utils import regularize_name
|
28
31
|
|
29
32
|
|
30
33
|
def print_wrap(s: str, newline=False, err=False, **kwargs):
|
@@ -58,9 +61,9 @@ class ConvertArgs:
|
|
58
61
|
self._output_dir: str | None = args.out
|
59
62
|
converter_name = getattr(args, "with")
|
60
63
|
if isinstance(converter_name, str):
|
61
|
-
self.name = converter_name
|
64
|
+
self.name = regularize_name(converter_name)
|
62
65
|
elif converter_name:
|
63
|
-
self.name
|
66
|
+
self.name = regularize_name(" ".join(converter_name))
|
64
67
|
else:
|
65
68
|
self.name = None
|
66
69
|
self.delete_input = args.delete_input
|
@@ -109,14 +112,14 @@ class ConvertArgs:
|
|
109
112
|
|
110
113
|
# Get the converter name from the arguments if it wasn't provided by -w/--with
|
111
114
|
if not self.name:
|
112
|
-
self.name = " ".join(self.l_args)
|
115
|
+
self.name = regularize_name(" ".join(self.l_args))
|
113
116
|
|
114
117
|
# For this operation, any other arguments can be ignored
|
115
118
|
return
|
116
119
|
|
117
120
|
# If not listing and a converter name wasn't supplied, use the default converter
|
118
121
|
if not self.name:
|
119
|
-
self.name = CONVERTER_DEFAULT
|
122
|
+
self.name = regularize_name(CONVERTER_DEFAULT)
|
120
123
|
|
121
124
|
# Quiet mode is equivalent to logging mode == LOGGING_NONE, so normalize them if either is set
|
122
125
|
if self.quiet:
|
@@ -147,13 +150,14 @@ class ConvertArgs:
|
|
147
150
|
os.makedirs(self._output_dir, exist_ok=True)
|
148
151
|
|
149
152
|
# Check the converter is recognized
|
150
|
-
if self.name
|
153
|
+
if not converter_is_supported(self.name):
|
151
154
|
msg = textwrap.fill(f"ERROR: Converter '{self.name}' not recognised", width=TERM_WIDTH)
|
152
155
|
msg += f"\n\n{get_supported_converters()}"
|
153
156
|
raise FileConverterInputException(msg, help=True, msg_preformatted=True)
|
154
|
-
elif self.name
|
155
|
-
|
156
|
-
|
157
|
+
elif not converter_is_registered(self.name):
|
158
|
+
converter_name = get_supported_converter_class(self.name).name
|
159
|
+
msg = textwrap.fill(f"ERROR: Converter '{converter_name}' is not registered. It may be possible to "
|
160
|
+
"register it by installing an appropriate binary for your platform.", width=TERM_WIDTH)
|
157
161
|
msg += f"\n\n{get_supported_converters()}"
|
158
162
|
raise FileConverterInputException(msg, help=True, msg_preformatted=True)
|
159
163
|
|
@@ -349,9 +353,10 @@ def detail_converter_use(args: ConvertArgs):
|
|
349
353
|
"""
|
350
354
|
|
351
355
|
converter_info = get_converter_info(args.name)
|
352
|
-
converter_class =
|
356
|
+
converter_class = get_supported_converter_class(args.name)
|
357
|
+
converter_name = converter_class.name
|
353
358
|
|
354
|
-
print_wrap(f"{
|
359
|
+
print_wrap(f"{converter_name}: {converter_info.description} ({converter_info.url})", break_long_words=False,
|
355
360
|
break_on_hyphens=False, newline=True)
|
356
361
|
|
357
362
|
if converter_class.info:
|
@@ -362,10 +367,10 @@ def detail_converter_use(args: ConvertArgs):
|
|
362
367
|
if args.from_format is not None and args.to_format is not None:
|
363
368
|
qual = get_conversion_quality(args.name, args.from_format, args.to_format)
|
364
369
|
if qual is None:
|
365
|
-
print_wrap(f"Conversion from '{args.from_format}' to '{args.to_format}' with {
|
370
|
+
print_wrap(f"Conversion from '{args.from_format}' to '{args.to_format}' with {converter_name} is not "
|
366
371
|
"supported.", newline=True)
|
367
372
|
else:
|
368
|
-
print_wrap(f"Conversion from '{args.from_format}' to '{args.to_format}' with {
|
373
|
+
print_wrap(f"Conversion from '{args.from_format}' to '{args.to_format}' with {converter_name} is "
|
369
374
|
f"possible with {qual.qual_str} conversion quality", newline=True)
|
370
375
|
# If there are any potential issues with the conversion, print them out
|
371
376
|
if qual.details:
|
@@ -385,7 +390,7 @@ def detail_converter_use(args: ConvertArgs):
|
|
385
390
|
optional_not: str = ""
|
386
391
|
else:
|
387
392
|
optional_not: str = "not "
|
388
|
-
print_wrap(f"Conversion {to_or_from} {format_name} is {optional_not}supported by {
|
393
|
+
print_wrap(f"Conversion {to_or_from} {format_name} is {optional_not}supported by {converter_name}.\n")
|
389
394
|
|
390
395
|
# List all possible formats, and which can be used for input and which for output
|
391
396
|
s_all_formats: set[FormatInfo] = set(l_input_formats)
|
@@ -393,7 +398,7 @@ def detail_converter_use(args: ConvertArgs):
|
|
393
398
|
l_all_formats: list[FormatInfo] = list(s_all_formats)
|
394
399
|
l_all_formats.sort(key=lambda x: x.disambiguated_name.lower())
|
395
400
|
|
396
|
-
print_wrap(f"File formats supported by {
|
401
|
+
print_wrap(f"File formats supported by {converter_name}:", newline=True)
|
397
402
|
max_format_length = max([len(x.disambiguated_name) for x in l_all_formats])
|
398
403
|
print(" "*(max_format_length+4) + " INPUT OUTPUT")
|
399
404
|
print(" "*(max_format_length+4) + " ----- ------")
|
@@ -472,13 +477,13 @@ def detail_converter_use(args: ConvertArgs):
|
|
472
477
|
# Now at the end, bring up input/output-format-specific flags and options
|
473
478
|
if mention_input_format and mention_output_format:
|
474
479
|
print_wrap("For details on input/output flags and options allowed for specific formats, call:\n"
|
475
|
-
f"{CL_SCRIPT_NAME} -l {
|
480
|
+
f"{CL_SCRIPT_NAME} -l {converter_name} -f <input_format> -t <output_format>")
|
476
481
|
elif mention_input_format:
|
477
482
|
print_wrap("For details on input flags and options allowed for a specific format, call:\n"
|
478
|
-
f"{CL_SCRIPT_NAME} -l {
|
483
|
+
f"{CL_SCRIPT_NAME} -l {converter_name} -f <input_format> [-t <output_format>]")
|
479
484
|
elif mention_output_format:
|
480
485
|
print_wrap("For details on output flags and options allowed for a specific format, call:\n"
|
481
|
-
f"{CL_SCRIPT_NAME} -l {
|
486
|
+
f"{CL_SCRIPT_NAME} -l {converter_name} -t <output_format> [-f <input_format>]")
|
482
487
|
|
483
488
|
|
484
489
|
def list_supported_formats(err=False):
|
@@ -537,7 +542,7 @@ def detail_format(format_name: str):
|
|
537
542
|
"""Prints details on a format
|
538
543
|
"""
|
539
544
|
|
540
|
-
l_format_info
|
545
|
+
l_format_info = get_format_info(format_name, which="all")
|
541
546
|
|
542
547
|
if len(l_format_info) == 0:
|
543
548
|
print_wrap(f"ERROR: Format '{format_name}' not recognised", err=True, newline=True)
|
@@ -600,9 +605,11 @@ def detail_formats_and_possible_converters(from_format: str, to_format: str):
|
|
600
605
|
l_conversions_matching_formats = [x for x in l_possible_conversions
|
601
606
|
if x[1] == possible_from_format and x[2] == possible_to_format]
|
602
607
|
|
603
|
-
l_possible_registered_converters = [x[0]
|
608
|
+
l_possible_registered_converters = [get_registered_converter_class(x[0]).name
|
609
|
+
for x in l_conversions_matching_formats
|
604
610
|
if x[0] in L_REGISTERED_CONVERTERS]
|
605
|
-
l_possible_unregistered_converters = [x[0]
|
611
|
+
l_possible_unregistered_converters = [get_supported_converter_class(x[0]).name
|
612
|
+
for x in l_conversions_matching_formats
|
606
613
|
if x[0] in L_SUPPORTED_CONVERTERS and x[0] not in L_REGISTERED_CONVERTERS]
|
607
614
|
|
608
615
|
print()
|
@@ -640,7 +647,7 @@ def get_supported_converters():
|
|
640
647
|
l_converters: list[str] = []
|
641
648
|
any_not_registered = False
|
642
649
|
for converter_name in L_SUPPORTED_CONVERTERS:
|
643
|
-
converter_text = converter_name
|
650
|
+
converter_text = get_supported_converter_class(converter_name).name
|
644
651
|
if converter_name not in L_REGISTERED_CONVERTERS:
|
645
652
|
converter_text += f" {MSG_NOT_REGISTERED}"
|
646
653
|
any_not_registered = True
|
@@ -17,16 +17,20 @@
|
|
17
17
|
</ul>
|
18
18
|
<ul class="footer__col footer__items clean-list">
|
19
19
|
<li>
|
20
|
-
<
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
<a href="https://www.ukri.org/">
|
21
|
+
<img class="lm-only" src="static/img/ukri-logo-lighttext.png"
|
22
|
+
alt="UKRI logo">
|
23
|
+
<img class="dm-only" src="static/img/ukri-logo-darktext.png"
|
24
|
+
alt="UKRI logo">
|
25
|
+
</a>
|
24
26
|
</li>
|
25
27
|
<li>
|
26
|
-
<
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
<a href="https://www.ukri.org/councils/epsrc/">
|
29
|
+
<img class="lm-only" src="static/img/ukri-epsr-logo-lighttext.png"
|
30
|
+
alt="UKRI EPSR logo">
|
31
|
+
<img class="dm-only" src="static/img/ukri-epsr-logo-darktext.png"
|
32
|
+
alt="UKRI EPSR logo">
|
33
|
+
</a>
|
30
34
|
</li>
|
31
35
|
</ul>
|
32
36
|
<div class="footer__hline small-screen-only">
|
@@ -44,7 +48,7 @@
|
|
44
48
|
<hr>
|
45
49
|
</div>
|
46
50
|
<ul class="footer__col footer__items clean-list">
|
47
|
-
<li><a href="https://
|
51
|
+
<li><a href="https://psdi.ac.uk/">PSDI Home</a></li>
|
48
52
|
<li><a href="mailto:support@psdi.ac.uk">Contact Us</a></li>
|
49
53
|
<li><a href="https://www.psdi.ac.uk/privacy/">Privacy</a></li>
|
50
54
|
<li><a href="https://www.psdi.ac.uk/terms-and-conditions/">Terms and Conditions</a></li>
|
@@ -4,7 +4,7 @@
|
|
4
4
|
<div class="max-width-box navbar">
|
5
5
|
<div class="header-left">
|
6
6
|
<div class="navbar__brand">
|
7
|
-
<a class="navbar__logo" href="https://
|
7
|
+
<a class="navbar__logo" href="https://psdi.ac.uk/">
|
8
8
|
<img src="static/img/psdi-logo-darktext.png" alt="PSDI logo"
|
9
9
|
class="lm-only">
|
10
10
|
<img src="static/img/psdi-logo-lighttext.png" alt="PSDI logo"
|
@@ -17,16 +17,20 @@
|
|
17
17
|
</ul>
|
18
18
|
<ul class="footer__col footer__items clean-list">
|
19
19
|
<li>
|
20
|
-
<
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
<a href="https://www.ukri.org/">
|
21
|
+
<img class="lm-only" src="../img/ukri-logo-lighttext.png"
|
22
|
+
alt="UKRI logo">
|
23
|
+
<img class="dm-only" src="../img/ukri-logo-darktext.png"
|
24
|
+
alt="UKRI logo">
|
25
|
+
</a>
|
24
26
|
</li>
|
25
27
|
<li>
|
26
|
-
<
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
<a href="https://www.ukri.org/councils/epsrc/">
|
29
|
+
<img class="lm-only" src="../img/ukri-epsr-logo-lighttext.png"
|
30
|
+
alt="UKRI EPSR logo">
|
31
|
+
<img class="dm-only" src="../img/ukri-epsr-logo-darktext.png"
|
32
|
+
alt="UKRI EPSR logo">
|
33
|
+
</a>
|
30
34
|
</li>
|
31
35
|
</ul>
|
32
36
|
<div class="footer__hline small-screen-only">
|
@@ -44,7 +48,7 @@
|
|
44
48
|
<hr>
|
45
49
|
</div>
|
46
50
|
<ul class="footer__col footer__items clean-list">
|
47
|
-
<li><a href="https://
|
51
|
+
<li><a href="https://psdi.ac.uk/">PSDI Home</a></li>
|
48
52
|
<li><a href="mailto:support@psdi.ac.uk">Contact Us</a></li>
|
49
53
|
<li><a href="https://www.psdi.ac.uk/privacy/">Privacy</a></li>
|
50
54
|
<li><a href="https://www.psdi.ac.uk/terms-and-conditions/">Terms and Conditions</a></li>
|
@@ -4,7 +4,7 @@
|
|
4
4
|
<div class="max-width-box navbar">
|
5
5
|
<div class="header-left">
|
6
6
|
<div class="navbar__brand">
|
7
|
-
<a class="navbar__logo" href="https://
|
7
|
+
<a class="navbar__logo" href="https://psdi.ac.uk/">
|
8
8
|
<img src="../img/psdi-logo-darktext.png" alt="PSDI logo"
|
9
9
|
class="lm-only">
|
10
10
|
<img src="../img/psdi-logo-lighttext.png" alt="PSDI logo"
|