CMIP7-data-request-api 1.1.2__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.
- CMIP7_data_request_api-1.1.2.dist-info/LICENSE +21 -0
- CMIP7_data_request_api-1.1.2.dist-info/METADATA +210 -0
- CMIP7_data_request_api-1.1.2.dist-info/RECORD +36 -0
- CMIP7_data_request_api-1.1.2.dist-info/WHEEL +5 -0
- CMIP7_data_request_api-1.1.2.dist-info/entry_points.txt +2 -0
- CMIP7_data_request_api-1.1.2.dist-info/top_level.txt +1 -0
- data_request_api/__init__.py +1 -0
- data_request_api/command_line/__init__.py +0 -0
- data_request_api/command_line/export_dreq_lists_json.py +136 -0
- data_request_api/dev/JA/__init__.py +0 -0
- data_request_api/dev/JA/check_plev_requests.py +416 -0
- data_request_api/dev/JA/read_feedback_spreadsheet.py +141 -0
- data_request_api/dev/JA/workflow_example_GRtest.py +275 -0
- data_request_api/dev/JA/workflow_example_test.py +222 -0
- data_request_api/dev/MM/checksum.py +68 -0
- data_request_api/dev/MM/walking_data_request.ipynb +380 -0
- data_request_api/dev/MS/dreq_content_and_walking_data_request.ipynb +3755 -0
- data_request_api/dev/__init__.py +0 -0
- data_request_api/stable/__init__.py +0 -0
- data_request_api/stable/content/README.MD +106 -0
- data_request_api/stable/content/__init__.py +0 -0
- data_request_api/stable/content/dreq_api/__init__.py +0 -0
- data_request_api/stable/content/dreq_api/consolidate_export.py +488 -0
- data_request_api/stable/content/dreq_api/dreq_content.py +593 -0
- data_request_api/stable/content/dreq_api/mapping_table.py +335 -0
- data_request_api/stable/content/dreq_api/test_dreq_content.py +194 -0
- data_request_api/stable/content/dump_transformation.py +550 -0
- data_request_api/stable/query/__init__.py +0 -0
- data_request_api/stable/query/data_request.py +1120 -0
- data_request_api/stable/query/dreq_classes.py +372 -0
- data_request_api/stable/query/dreq_query.py +981 -0
- data_request_api/stable/query/vocabulary_server.py +208 -0
- data_request_api/stable/utilities/__init__.py +0 -0
- data_request_api/stable/utilities/logger.py +71 -0
- data_request_api/stable/utilities/tools.py +49 -0
- data_request_api/version.py +16 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Script to change the basic airtable export into readable files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import division, print_function, unicode_literals, absolute_import
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import argparse
|
|
14
|
+
import re
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from data_request_api.stable.utilities.logger import get_logger, change_log_level, change_log_file
|
|
19
|
+
from data_request_api.stable.utilities.tools import read_json_input_file_content, write_json_output_file_content
|
|
20
|
+
from .dreq_api import dreq_content as dc
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def correct_key_string(input_string, *to_remove_strings):
|
|
24
|
+
"""
|
|
25
|
+
Change the input string by replacing '&' by 'and' and spaces by underscores.
|
|
26
|
+
It also removes others specified strings.
|
|
27
|
+
:param str input_string: the input string to be changed
|
|
28
|
+
:param list of str to_remove_strings: the list of strings to be removed from input_string
|
|
29
|
+
:return str: the changed string
|
|
30
|
+
"""
|
|
31
|
+
logger = get_logger()
|
|
32
|
+
if isinstance(input_string, str):
|
|
33
|
+
input_string = input_string.lower()
|
|
34
|
+
for to_remove_string in to_remove_strings:
|
|
35
|
+
input_string = input_string.replace(to_remove_string.lower(), "")
|
|
36
|
+
input_string = input_string.strip()
|
|
37
|
+
input_string = input_string.replace("&", "and").replace(" ", "_")
|
|
38
|
+
else:
|
|
39
|
+
logger.error(f"Deal with string types, not {type(input_string).__name__}")
|
|
40
|
+
raise TypeError(f"Deal with string types, not {type(input_string).__name__}")
|
|
41
|
+
return input_string
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def correct_dictionaries(input_dict, is_record_ids=False):
|
|
45
|
+
"""
|
|
46
|
+
Correct the input_dict to correct the strings except the record ids.
|
|
47
|
+
:param dict input_dict: the input dictionary to be corrected
|
|
48
|
+
:param bool is_record_ids: a boolean to indicate whether the keys of input_dict contain record ids or not
|
|
49
|
+
:return dict: the corrected dictionary
|
|
50
|
+
"""
|
|
51
|
+
logger = get_logger()
|
|
52
|
+
if isinstance(input_dict, dict):
|
|
53
|
+
rep = dict()
|
|
54
|
+
for (key, value) in input_dict.items():
|
|
55
|
+
if not is_record_ids:
|
|
56
|
+
new_key = correct_key_string(key)
|
|
57
|
+
else:
|
|
58
|
+
new_key = key
|
|
59
|
+
if isinstance(value, dict):
|
|
60
|
+
rep[new_key] = correct_dictionaries(value, is_record_ids=key in ["records", "fields"])
|
|
61
|
+
else:
|
|
62
|
+
rep[new_key] = copy.deepcopy(value)
|
|
63
|
+
return rep
|
|
64
|
+
else:
|
|
65
|
+
logger.error(f"Deal with dict types, not {type(input_dict).__name__}")
|
|
66
|
+
raise TypeError(f"Deal with dict types, not {type(input_dict).__name__}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def transform_content_three_bases(content):
|
|
70
|
+
"""
|
|
71
|
+
Transform the several bases export content into something similar to a one base export content.
|
|
72
|
+
To do that, the content of the different entries of the input dictionary are copied to a single dictionary.
|
|
73
|
+
The record ids are also harmonised through the different bases.
|
|
74
|
+
:param dict content: input dictionary containing the different databases
|
|
75
|
+
:return dict: dictionary containing the content of the different databases
|
|
76
|
+
"""
|
|
77
|
+
logger = get_logger()
|
|
78
|
+
if isinstance(content, dict) and len(content) > 2:
|
|
79
|
+
new_content = dict()
|
|
80
|
+
opportunity_table = [elt for elt in list(content) if "opportunities" in elt.lower()][0]
|
|
81
|
+
variables_table = [elt for elt in list(content) if "variables" in elt.lower()][0]
|
|
82
|
+
physical_parameters_table = [elt for elt in list(content) if "parameters" in elt.lower()][0]
|
|
83
|
+
# Copy the bases
|
|
84
|
+
old_variables_content = content[opportunity_table].pop("Variables")
|
|
85
|
+
old_physical_parameters_content = content[variables_table].pop("Physical Parameter")
|
|
86
|
+
new_content["Opportunity/variable Group Comments"] = content[opportunity_table].pop("Comment")
|
|
87
|
+
new_content["Experiments"] = content[opportunity_table].pop("Experiment")
|
|
88
|
+
new_content["MIPs"] = content[opportunity_table].pop("MIP")
|
|
89
|
+
for elt in list(content[opportunity_table]):
|
|
90
|
+
new_content[elt] = content[opportunity_table].pop(elt)
|
|
91
|
+
new_content["Variables"] = content[variables_table].pop("Variable")
|
|
92
|
+
new_content["Coordinates and Dimensions"] = content[variables_table].pop("Coordinate or Dimension")
|
|
93
|
+
new_content["Variable Comments"] = content[variables_table].pop("Comment")
|
|
94
|
+
if "Modeling Realm" in content[variables_table]:
|
|
95
|
+
new_content["Modelling Realm"] = content[variables_table].pop("Modeling Realm")
|
|
96
|
+
for elt in list(content[variables_table]):
|
|
97
|
+
new_content[elt] = content[variables_table].pop(elt)
|
|
98
|
+
new_content["Physical Parameter Comments"] = content[physical_parameters_table].pop("Comment")
|
|
99
|
+
new_content["Physical Parameters"] = content[physical_parameters_table].pop("Physical Parameter")
|
|
100
|
+
new_content["CF Standard Names"] = content[physical_parameters_table].pop("CF Standard Name")
|
|
101
|
+
for elt in list(content[physical_parameters_table]):
|
|
102
|
+
new_content[elt] = content[physical_parameters_table].pop(elt)
|
|
103
|
+
# Correct record id through several bases
|
|
104
|
+
old_variables_ids = {record_id: value["Compound Name"] for (record_id, value) in
|
|
105
|
+
old_variables_content["records"].items()}
|
|
106
|
+
new_variables_ids = {value["Compound Name"]: record_id for (record_id, value) in
|
|
107
|
+
new_content["Variables"]["records"].items()}
|
|
108
|
+
for var_group_id in list(new_content["Variable Group"]["records"]):
|
|
109
|
+
new_content["Variable Group"]["records"][var_group_id]["Variables"] = \
|
|
110
|
+
[new_variables_ids[old_variables_ids[elt]] for elt in
|
|
111
|
+
new_content["Variable Group"]["records"][var_group_id]["Variables"]]
|
|
112
|
+
old_physical_parameters_ids = {record_id: value["Name"] for (record_id, value) in
|
|
113
|
+
old_physical_parameters_content["records"].items()}
|
|
114
|
+
new_physical_parameters_ids = {value["Name"]: record_id for (record_id, value) in
|
|
115
|
+
new_content["Physical Parameters"]["records"].items()}
|
|
116
|
+
for var_id in list(new_content["Variables"]["records"]):
|
|
117
|
+
if "Physical Parameter" not in new_content["Variables"]["records"][var_id]:
|
|
118
|
+
logger.debug(f"Remove Variables record ID {var_id}, no 'Physical Parameter' field defined.")
|
|
119
|
+
del new_content["Variables"]["records"][var_id]
|
|
120
|
+
else:
|
|
121
|
+
new_content["Variables"]["records"][var_id]["Physical Parameter"] = \
|
|
122
|
+
[new_physical_parameters_ids[old_physical_parameters_ids[elt]] for elt in
|
|
123
|
+
new_content["Variables"]["records"][var_id]["Physical Parameter"]]
|
|
124
|
+
# Harmonise record ids through bases
|
|
125
|
+
logger.info("Harmonise bases content record ids")
|
|
126
|
+
content_str = json.dumps(new_content)
|
|
127
|
+
for id in sorted(list(old_variables_ids)):
|
|
128
|
+
content_str = re.sub(f'"{id}"', f'"{new_variables_ids[old_variables_ids[id]]}"', content_str)
|
|
129
|
+
for id in sorted(list(old_physical_parameters_ids)):
|
|
130
|
+
content_str = re.sub(f'"{id}"', f'"{new_physical_parameters_ids[old_physical_parameters_ids[id]]}"', content_str)
|
|
131
|
+
new_content = json.loads(content_str)
|
|
132
|
+
# Return the content
|
|
133
|
+
return {"Data Request": new_content}
|
|
134
|
+
elif isinstance(content, dict):
|
|
135
|
+
logger.error(f"Deal with several bases dict.")
|
|
136
|
+
raise ValueError(f"Deal with several bases dict.")
|
|
137
|
+
else:
|
|
138
|
+
logger.error(f"Deal with dict types, not {type(content).__name__}")
|
|
139
|
+
raise TypeError(f"Deal with dict types, not {type(content).__name__}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def transform_content_one_base(content):
|
|
143
|
+
"""
|
|
144
|
+
Transform a one base export content to:
|
|
145
|
+
- remove unused keys which could create circle import later
|
|
146
|
+
- harmonise some entries
|
|
147
|
+
- reshape entries if needed
|
|
148
|
+
- remove elements which are not used
|
|
149
|
+
- filter content on status
|
|
150
|
+
:param dict content: one base content export (direct export or created from `transform_content_three_bases`
|
|
151
|
+
:return dict: the transform content
|
|
152
|
+
"""
|
|
153
|
+
logger = get_logger()
|
|
154
|
+
if isinstance(content, dict) and len(content) == 1:
|
|
155
|
+
default_count = 0
|
|
156
|
+
default_template = "default_{:d}"
|
|
157
|
+
content = content[list(content)[0]]
|
|
158
|
+
# Rename some elements
|
|
159
|
+
esm_bcv_regexp = re.compile(r"esm-bcv.*")
|
|
160
|
+
esm_bcv = [elt for elt in list(content) if esm_bcv_regexp.match(elt)]
|
|
161
|
+
if len(esm_bcv) == 1:
|
|
162
|
+
esm_bcv = esm_bcv[0]
|
|
163
|
+
content["esm-bcv"] = content.pop(esm_bcv)
|
|
164
|
+
for (key, new_key) in [("opportunity", "opportunities"), ("experiment_group", "experiment_groups"),
|
|
165
|
+
("variable_group", "variable_groups"), ("structure", "structure_title"),
|
|
166
|
+
("time_slice", "time_subset")]:
|
|
167
|
+
if key in content:
|
|
168
|
+
content[new_key] = content.pop(key)
|
|
169
|
+
for pattern in [".*rank.*", ]:
|
|
170
|
+
elts = [elt for elt in list(content) if re.compile(pattern).match(elt)]
|
|
171
|
+
for elt in elts:
|
|
172
|
+
del content[elt]
|
|
173
|
+
for pattern in ["(legacy)", ]:
|
|
174
|
+
for elt in [elt for elt in list(content) if pattern in elt]:
|
|
175
|
+
new_elt = elt.replace(pattern, "").strip("_")
|
|
176
|
+
content[new_elt] = content.pop(elt)
|
|
177
|
+
# Tidy the content of the export file
|
|
178
|
+
default_patterns_to_remove = [r".*\(from.*\).*", r".*proposed.*", r".*review.*", r".*--.*",
|
|
179
|
+
r".*created.*", r".*rank.*", ".*count.*", ".*alert.*", ".*tagged.*", ".*unique.*",
|
|
180
|
+
"last_modified.*", ".*validation.*", ".*number.*", ".*mj.*", r".*proposal.*"]
|
|
181
|
+
to_remove_keys_patterns = {
|
|
182
|
+
"cell_measures": [r"variables", "structure"],
|
|
183
|
+
"cell_methods": [r"structure", r"variables"],
|
|
184
|
+
"cf_standard_names": [r"physical_parameters.*", "esm-bcv.*"],
|
|
185
|
+
"cmip6_frequency": [r"table_identifiers.*", r"variables.*"],
|
|
186
|
+
"cmip7_frequency": [r"table_identifiers.*", r"variables.*"],
|
|
187
|
+
"coordinates_and_dimensions": [r"spatial_shape", r"structure", "temporal_shape", "variables", "size"],
|
|
188
|
+
"data_request_themes": [r"experiment_group.*", r".*opportunit.*", r"variable_group.*"],
|
|
189
|
+
"esm-bcv": [r"v\d.*", "cf_standard_name", ".*variables"],
|
|
190
|
+
"experiment_groups": [r"opportunit.*", r"theme.*", "comments.+"],
|
|
191
|
+
"experiments": [r"experiment_group.*", r"opportunit.*", "variables", "mip"],
|
|
192
|
+
"glossary": ["opportunit.*", ],
|
|
193
|
+
"mips": ["variable_group.*", "experiments.*", ".*opportunit.*", "variables"],
|
|
194
|
+
"modelling_realm": ["variables", ],
|
|
195
|
+
"opportunities": [".*data_volume_estimate", "opportunity_id", "originally_requested_variable_groups"],
|
|
196
|
+
"opportunity/variable_group_comments": ["experiment_groups", "opportunities", "theme", "variable_groups"],
|
|
197
|
+
"physical_parameters": ["variables", "conditional", "does_a_cf.*"],
|
|
198
|
+
"physical_parameter_comments": ["physical_parameters", "does_a.*", "cf_standard_names", "physical_parameters"],
|
|
199
|
+
"priority_level": ["variable_group", ],
|
|
200
|
+
"spatial_shape": [r"dimensions.*", r"structure.*", r".*variables.*", "hor.*", "vert.*"],
|
|
201
|
+
"structure_title": [r"variables.*", "brand_.*", "calculation.*"],
|
|
202
|
+
"table_identifiers": ["variables", ],
|
|
203
|
+
"temporal_shape": ["variables", "structure"],
|
|
204
|
+
"time_subset": ["uid.+", "opportunit.*"],
|
|
205
|
+
"variable_comments": ["variable.*", "spatial_shape", "temporal_shape", "coordinates_and_dimensions",
|
|
206
|
+
"cell_methods", "cell_measures"],
|
|
207
|
+
"variable_groups": [".*opportunit.*", "theme", r"size.*", "mip_ownership"],
|
|
208
|
+
"variables": [r"priority.*", r".*variable_group.*", ".*experiment.*", "size", "vertical_dimension",
|
|
209
|
+
"temporal_sampling_rate", "horizontal_mesh", r"brand.*\[link\]", "structure_label",
|
|
210
|
+
"table_section.*", "theme"],
|
|
211
|
+
}
|
|
212
|
+
to_rename_keys_patterns = {
|
|
213
|
+
"cell_methods": [("comments", "variable_comments"), ("label", "name")],
|
|
214
|
+
"cf_standard_names": [("comments", "physical_parameter_comments")],
|
|
215
|
+
"cmip7_frequency": [("cmip6_frequency.*", "cmip6_frequency")],
|
|
216
|
+
"coordinates_and_dimensions": [("requested_bounds.+", "requested_bounds"), ("comments", "variable_comments")],
|
|
217
|
+
"data_request_themes": [("comments", "opportunity/variable_group_comments"), ("uid.+", "uid")],
|
|
218
|
+
"experiments": [("experiment", "name")],
|
|
219
|
+
"experiment_groups": [("comments", "opportunity/variable_group_comments")],
|
|
220
|
+
"modelling_realm": [("id", "uid")],
|
|
221
|
+
"opportunities": [("title_of_opportunity", "name"), ("comments", "opportunity/variable_group_comments"),
|
|
222
|
+
("ensemble_size", "minimum_ensemble_size"), ("themes", "data_request_themes"),
|
|
223
|
+
("working/updated_variable_groups", "variable_groups"), ("time_slice", "time_subset")],
|
|
224
|
+
"physical_parameters": [("comments", "physical_parameter_comments"),
|
|
225
|
+
("cf_proposal_github_issue", "proposal_github_issue"),
|
|
226
|
+
("flag.*change.*", "flag_change_since_cmip6")],
|
|
227
|
+
"spatial_shape": [("comments", "variable_comments")],
|
|
228
|
+
"temporal_shape": [("comments", "variable_comments")],
|
|
229
|
+
"structure_title": [("label", "name")],
|
|
230
|
+
"table_identifiers": [("comment", "notes"), ("frequency", "cmip6_frequency")],
|
|
231
|
+
"time_subset": [("label", "name")],
|
|
232
|
+
"variable_groups": [(".*mips.*", "mips"), ("comments", "opportunity/variable_group_comments")],
|
|
233
|
+
"variables": [("compound_name", "name"), ("cmip6_frequency.+", "cmip6_frequency"), (esm_bcv, "esm-bcv"),
|
|
234
|
+
("modeling_realm", "modelling_realm"), ("comments", "variable_comments"),
|
|
235
|
+
("table", "table_identifier")],
|
|
236
|
+
}
|
|
237
|
+
to_merge_keys_patterns = {
|
|
238
|
+
"opportunities": [("mips.*", "mips"), ] # (".+variable_groups", "variable_groups")]
|
|
239
|
+
}
|
|
240
|
+
to_sort_keys_content = {
|
|
241
|
+
"opportunities": ["variable_groups", "data_request_themes", "experiment_groups", "time_slice"],
|
|
242
|
+
"experiment_groups": ["experiments", ],
|
|
243
|
+
"variable_groups": ["variables", "mips"]
|
|
244
|
+
}
|
|
245
|
+
from_list_to_string_keys_content = {
|
|
246
|
+
"opportunities": ["lead_theme", ],
|
|
247
|
+
"physical_parameters": ["cf_standard_name", ],
|
|
248
|
+
"table_identifiers": ["cmip7_frequency", ],
|
|
249
|
+
"variables": ["cell_methods", "cmip6_frequency", "cmip7_frequency", "esm-bcv", "physical_parameter",
|
|
250
|
+
"spatial_shape", "table_identifier", "temporal_shape"]
|
|
251
|
+
}
|
|
252
|
+
for subelt in sorted(list(content)):
|
|
253
|
+
# Remove everything save records
|
|
254
|
+
records = content[subelt].pop("records")
|
|
255
|
+
for subkey in list(content[subelt]):
|
|
256
|
+
del content[subelt][subkey]
|
|
257
|
+
content[subelt].update(records)
|
|
258
|
+
# Find out list of patterns to remove, rename, merge, sort...
|
|
259
|
+
patterns_to_remove = to_remove_keys_patterns.get(subelt, list())
|
|
260
|
+
patterns_to_remove.extend(default_patterns_to_remove)
|
|
261
|
+
patterns_to_remove = [re.compile(elt) for elt in patterns_to_remove]
|
|
262
|
+
patterns_to_rename = to_rename_keys_patterns.get(subelt, list())
|
|
263
|
+
patterns_to_rename = [(re.compile(elt[0]), elt[1]) if not isinstance(elt[0], list) else elt for elt in patterns_to_rename]
|
|
264
|
+
patterns_to_merge = to_merge_keys_patterns.get(subelt, list())
|
|
265
|
+
patterns_to_merge = [(re.compile(elt[0]), elt[1]) for elt in patterns_to_merge]
|
|
266
|
+
for record_id in sorted(list(content[subelt])):
|
|
267
|
+
# Remove unused keys
|
|
268
|
+
list_keys = sorted(list(content[subelt][record_id]))
|
|
269
|
+
list_keys_to_remove = [elt for elt in list_keys if
|
|
270
|
+
any(patt.match(elt) is not None for patt in patterns_to_remove)]
|
|
271
|
+
for key in list_keys_to_remove:
|
|
272
|
+
del content[subelt][record_id][key]
|
|
273
|
+
# Rename needed keys
|
|
274
|
+
list_keys = sorted(list(content[subelt][record_id]))
|
|
275
|
+
for (patt, repl) in patterns_to_rename:
|
|
276
|
+
if isinstance(patt, list) and len(patt) == 0:
|
|
277
|
+
if repl in ["esm-bcv", ]:
|
|
278
|
+
to_rename = [elt for elt in list_keys if esm_bcv_regexp.match(elt) is not None]
|
|
279
|
+
else:
|
|
280
|
+
raise ValueError(f"Issue with patt void list with replacement {repl}.")
|
|
281
|
+
else:
|
|
282
|
+
to_rename = [elt for elt in list_keys if patt.match(elt) is not None]
|
|
283
|
+
if len(to_rename) == 1:
|
|
284
|
+
content[subelt][record_id][repl] = content[subelt][record_id].pop(to_rename[0])
|
|
285
|
+
elif len(to_rename) > 1:
|
|
286
|
+
raise ValueError(f"Several keys ({to_rename}) match pattern {patt} in subelt {subelt}.")
|
|
287
|
+
# Merge needed keys
|
|
288
|
+
list_keys = sorted(list(content[subelt][record_id]))
|
|
289
|
+
for (patt, repl) in patterns_to_merge:
|
|
290
|
+
to_merge = [elt for elt in list_keys if patt.match(elt) is not None]
|
|
291
|
+
if len(to_merge) > 0:
|
|
292
|
+
content[subelt][record_id][repl] = list()
|
|
293
|
+
for elts in to_merge:
|
|
294
|
+
if isinstance(content[subelt][record_id][elts], list):
|
|
295
|
+
content[subelt][record_id][repl].extend(content[subelt][record_id].pop(elts))
|
|
296
|
+
else:
|
|
297
|
+
content[subelt][record_id][repl].append(content[subelt][record_id].pop(elts))
|
|
298
|
+
# Add keys if needed
|
|
299
|
+
list_keys = sorted(list(set(content[subelt][record_id])))
|
|
300
|
+
if "name" not in list_keys:
|
|
301
|
+
content[subelt][record_id]["name"] = "undef"
|
|
302
|
+
# Filter on status if needed then remove linked keys
|
|
303
|
+
variable_groups = set()
|
|
304
|
+
experiment_groups = set()
|
|
305
|
+
variables = set()
|
|
306
|
+
experiments = set()
|
|
307
|
+
subelt = "opportunities"
|
|
308
|
+
for record_id in sorted(list(content[subelt])):
|
|
309
|
+
if content[subelt][record_id].get("status") not in ["Accepted", "Under review", None]:
|
|
310
|
+
del content[subelt][record_id]
|
|
311
|
+
else:
|
|
312
|
+
variable_groups = variable_groups | set(content[subelt][record_id].get("variable_groups", list()))
|
|
313
|
+
experiment_groups = experiment_groups | set(content[subelt][record_id].get("experiment_groups", list()))
|
|
314
|
+
subelt = "variable_groups"
|
|
315
|
+
for record_id in sorted(list(content[subelt])):
|
|
316
|
+
if record_id not in variable_groups:
|
|
317
|
+
del content[subelt][record_id]
|
|
318
|
+
else:
|
|
319
|
+
variables = variables | set(content[subelt][record_id].get("variables", list()))
|
|
320
|
+
subelt = "experiment_groups"
|
|
321
|
+
for record_id in sorted(list(content[subelt])):
|
|
322
|
+
if record_id not in experiment_groups:
|
|
323
|
+
del content[subelt][record_id]
|
|
324
|
+
elif content[subelt][record_id].get("status") in ["Junk", ]:
|
|
325
|
+
del content[subelt][record_id]
|
|
326
|
+
for op in list(content["opportunities"]):
|
|
327
|
+
if record_id in content["opportunities"][op]["experiment_groups"]:
|
|
328
|
+
content["opportunities"][op]["experiment_groups"].remove(record_id)
|
|
329
|
+
else:
|
|
330
|
+
experiments = experiments | set(content[subelt][record_id].get("experiments", list()))
|
|
331
|
+
subelt = "variables"
|
|
332
|
+
for record_id in sorted(list(set(content[subelt]) - variables)):
|
|
333
|
+
del content[subelt][record_id]
|
|
334
|
+
subelt = "experiments"
|
|
335
|
+
for record_id in sorted(list(set(content[subelt]) - experiments)):
|
|
336
|
+
del content[subelt][record_id]
|
|
337
|
+
for subelt in list(content):
|
|
338
|
+
for record_id in list(content[subelt]):
|
|
339
|
+
for key in [key for key in list(content[subelt][record_id])
|
|
340
|
+
if re.compile(r".*status.*").match(key) is not None]:
|
|
341
|
+
del content[subelt][record_id][key]
|
|
342
|
+
# Add uid if needed
|
|
343
|
+
record_to_uid_index = dict()
|
|
344
|
+
for subelt in sorted(list(content)):
|
|
345
|
+
for record_id in sorted(list(content[subelt]),
|
|
346
|
+
key=lambda record_id: "|".join([content[subelt][record_id].get("name"),
|
|
347
|
+
content[subelt][record_id].get("uid", "undef"),
|
|
348
|
+
record_id])):
|
|
349
|
+
if "uid" not in content[subelt][record_id]:
|
|
350
|
+
uid = default_template.format(default_count)
|
|
351
|
+
content[subelt][record_id]["uid"] = uid
|
|
352
|
+
default_count += 1
|
|
353
|
+
logger.debug(f"Undefined uid for element {os.sep.join([subelt, 'records', record_id])}, set {uid}")
|
|
354
|
+
uid = content[subelt][record_id].pop("uid")
|
|
355
|
+
if uid.endswith(os.linesep):
|
|
356
|
+
logger.debug(f"uid of element type {subelt} and record id {record_id} endswith '\\n'.")
|
|
357
|
+
uid = uid.rstrip(os.linesep)
|
|
358
|
+
record_to_uid_index[record_id] = (uid, subelt)
|
|
359
|
+
content[subelt][uid] = content[subelt].pop(record_id)
|
|
360
|
+
# Replace record_id by uid
|
|
361
|
+
logger.debug("Replace record ids by uids")
|
|
362
|
+
to_remove_entries = defaultdict(list)
|
|
363
|
+
content_string = json.dumps(content)
|
|
364
|
+
for (record_id, (uid, subelt)) in record_to_uid_index.items():
|
|
365
|
+
(content_string, nb) = re.subn(f'"{record_id}"', f'"link::{uid}"', content_string)
|
|
366
|
+
if nb == 0:
|
|
367
|
+
to_remove_entries[subelt].append((record_id, uid))
|
|
368
|
+
for record_id, _ in to_remove_entries["opportunities"]:
|
|
369
|
+
del record_to_uid_index[record_id]
|
|
370
|
+
del to_remove_entries["opportunities"]
|
|
371
|
+
content = json.loads(content_string)
|
|
372
|
+
# Remove unused entries
|
|
373
|
+
for subelt in to_remove_entries:
|
|
374
|
+
for (record_id, uid) in to_remove_entries[subelt]:
|
|
375
|
+
del content[subelt][uid]
|
|
376
|
+
del record_to_uid_index[record_id]
|
|
377
|
+
# Tidy the content once again
|
|
378
|
+
content_str = json.dumps(content)
|
|
379
|
+
to_remove_entries = defaultdict(list)
|
|
380
|
+
for (record_id, (uid, subelt)) in record_to_uid_index.items():
|
|
381
|
+
nb = content_str.count(uid)
|
|
382
|
+
if nb < 2:
|
|
383
|
+
to_remove_entries[subelt].append(uid)
|
|
384
|
+
for subelt in to_remove_entries:
|
|
385
|
+
for uid in to_remove_entries[subelt]:
|
|
386
|
+
del content[subelt][uid]
|
|
387
|
+
# Sort content of needed keys
|
|
388
|
+
for subelt in sorted(list(content)):
|
|
389
|
+
patterns_to_sort = to_sort_keys_content.get(subelt, list())
|
|
390
|
+
patterns_to_sort = [re.compile(elt) for elt in patterns_to_sort]
|
|
391
|
+
patterns_to_reshape = from_list_to_string_keys_content.get(subelt, list())
|
|
392
|
+
patterns_to_reshape = [re.compile(elt) for elt in patterns_to_reshape]
|
|
393
|
+
for uid in sorted(list(content[subelt])):
|
|
394
|
+
# Sort content of needed keys
|
|
395
|
+
list_keys = sorted(list(content[subelt][uid]))
|
|
396
|
+
list_keys_to_sort = [elt for elt in list_keys
|
|
397
|
+
if any(patt.match(elt) is not None for patt in patterns_to_sort)]
|
|
398
|
+
for key in list_keys_to_sort:
|
|
399
|
+
content[subelt][uid][key] = sorted(list(set(content[subelt][uid][key])))
|
|
400
|
+
# Reshape content if needed
|
|
401
|
+
list_keys_to_reshape = [elt for elt in list_keys
|
|
402
|
+
if any(patt.match(elt) is not None for patt in patterns_to_reshape)]
|
|
403
|
+
for key in list_keys_to_reshape:
|
|
404
|
+
if isinstance(content[subelt][uid][key], list):
|
|
405
|
+
if len(content[subelt][uid][key]) == 1:
|
|
406
|
+
content[subelt][uid][key] = content[subelt][uid][key][0]
|
|
407
|
+
elif len(content[subelt][uid][key]) == 0:
|
|
408
|
+
logger.warning(f"Remove void key {key} from id {uid} of element type {subelt}")
|
|
409
|
+
del content[subelt][uid][key]
|
|
410
|
+
else:
|
|
411
|
+
logger.error(f"Could not reshape key {key} from id {uid} of element type {subelt}: contains several elements")
|
|
412
|
+
raise ValueError(f"Could not reshape key {key} from id {uid} of element type {subelt}: contains several elements")
|
|
413
|
+
elif isinstance(content[subelt][uid][key], str):
|
|
414
|
+
logger.warning(f"Could not reshape key {key} from id {uid} of element type {subelt}: already a string")
|
|
415
|
+
else:
|
|
416
|
+
logger.error(f"Could not reshape key {key} from id {uid} of element type {subelt}: not a list")
|
|
417
|
+
raise ValueError(f"Could not reshape key {key} from id {uid} of element type {subelt}: not a list")
|
|
418
|
+
return content
|
|
419
|
+
elif isinstance(content, dict):
|
|
420
|
+
logger.error("Deal with one base content dict.")
|
|
421
|
+
raise ValueError("Deal with one base content dict.")
|
|
422
|
+
else:
|
|
423
|
+
logger.error(f"Deal with dict types, not {type(content).__name__}")
|
|
424
|
+
raise TypeError(f"Deal with dict types, not {type(content).__name__}")
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def split_content_one_base(content):
|
|
428
|
+
"""
|
|
429
|
+
Split the one base content into two dictionaries:
|
|
430
|
+
- the DR (structure)
|
|
431
|
+
- the VS (vocabulary server with all information)
|
|
432
|
+
:param dict content: dictionary containing the one base content
|
|
433
|
+
:return dict, dict: two dictionaries containing respectively the DR and VS
|
|
434
|
+
"""
|
|
435
|
+
logger = get_logger()
|
|
436
|
+
data_request = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: dict)))
|
|
437
|
+
keys_to_dr_dict = {
|
|
438
|
+
"opportunities": [("experiment_groups", list, list()),
|
|
439
|
+
("variable_groups", list, list()),
|
|
440
|
+
("data_request_themes", list, list()),
|
|
441
|
+
("time_subset", list, list()),
|
|
442
|
+
("mips", list, list())],
|
|
443
|
+
"variable_groups": [("variables", list, list()),
|
|
444
|
+
("mips", list, list()),
|
|
445
|
+
("priority_level", (str, type(None)), None)],
|
|
446
|
+
"experiment_groups": [("experiments", list, list()), ]
|
|
447
|
+
}
|
|
448
|
+
if isinstance(content, dict):
|
|
449
|
+
logger.debug("Build DR and VS")
|
|
450
|
+
for subelt in sorted(list(content)):
|
|
451
|
+
if subelt in keys_to_dr_dict:
|
|
452
|
+
for uid in content[subelt]:
|
|
453
|
+
for (key, target_type, default) in keys_to_dr_dict[subelt]:
|
|
454
|
+
value = content[subelt][uid].pop(key, default)
|
|
455
|
+
if not isinstance(value, target_type):
|
|
456
|
+
if target_type in [list, ] and isinstance(value, (str, int, type(None))):
|
|
457
|
+
value = [value, ]
|
|
458
|
+
elif str in target_type and isinstance(value, list):
|
|
459
|
+
value = value[0]
|
|
460
|
+
else:
|
|
461
|
+
raise TypeError(f"Could not deal with target type {type(target_type)}")
|
|
462
|
+
data_request[subelt][uid][key] = value
|
|
463
|
+
return data_request, content
|
|
464
|
+
else:
|
|
465
|
+
logger.error(f"Deal with dict types, not {type(content).__name__}")
|
|
466
|
+
raise TypeError(f"Deal with dict types, not {type(content).__name__}")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def transform_content(content, version):
|
|
470
|
+
"""
|
|
471
|
+
Function to transform the export content (single or several base-s- export) to VS and DR dictionaries.
|
|
472
|
+
The key "version" is added to the DR and VS dictionaries.
|
|
473
|
+
:param dict content: input export content (either single base or several bases)
|
|
474
|
+
:param str version: string containing the version of the export content
|
|
475
|
+
:return dict, dict: DR and VS dictionaries containing respectively the structure (DR) and the vocabulary (VS)
|
|
476
|
+
"""
|
|
477
|
+
logger = get_logger()
|
|
478
|
+
if isinstance(content, dict):
|
|
479
|
+
# Get back to one database case if needed
|
|
480
|
+
if len(content) == 1:
|
|
481
|
+
logger.info("Single database case - no structure transformation needed")
|
|
482
|
+
elif len(content) in [3, 4]:
|
|
483
|
+
logger.info("Several databases case - structure transformation needed")
|
|
484
|
+
content = transform_content_three_bases(content)
|
|
485
|
+
else:
|
|
486
|
+
raise ValueError(f"Could not manage the {len(content):d} bases export file.")
|
|
487
|
+
# Correct dictionaries
|
|
488
|
+
content = correct_dictionaries(content)
|
|
489
|
+
# Change several attributes
|
|
490
|
+
content = transform_content_one_base(content)
|
|
491
|
+
# Separate DR and VS files
|
|
492
|
+
data_request, vocabulary_server = split_content_one_base(content)
|
|
493
|
+
data_request["version"] = version
|
|
494
|
+
vocabulary_server["version"] = version
|
|
495
|
+
return data_request, vocabulary_server
|
|
496
|
+
else:
|
|
497
|
+
logger.error(f"Deal with dict types, not {type(content).__name__}")
|
|
498
|
+
raise TypeError(f"Deal with dict types, not {type(content).__name__}")
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def get_transformed_content(version="latest_stable", export_version="release", use_consolidation=False,
|
|
502
|
+
force_retrieve=False, output_dir=None,
|
|
503
|
+
default_transformed_content_pattern="{kind}_{export_version}_content.json"):
|
|
504
|
+
# Download specified version of data request content (if not locally cached)
|
|
505
|
+
versions = dc.retrieve(version, export=export_version, consolidate=use_consolidation)
|
|
506
|
+
|
|
507
|
+
# Check that there is only one version associated
|
|
508
|
+
if len(versions) > 1:
|
|
509
|
+
raise ValueError("Could only deal with one version.")
|
|
510
|
+
elif len(versions) == 0:
|
|
511
|
+
raise ValueError("No version found.")
|
|
512
|
+
else:
|
|
513
|
+
version = list(versions)[0]
|
|
514
|
+
content = versions[version]
|
|
515
|
+
if output_dir is None:
|
|
516
|
+
output_dir = os.path.dirname(content)
|
|
517
|
+
if not os.path.exists(output_dir):
|
|
518
|
+
os.makedirs(output_dir)
|
|
519
|
+
DR_content = default_transformed_content_pattern.format(kind="DR", export_version=export_version)
|
|
520
|
+
VS_content = default_transformed_content_pattern.format(kind="VS", export_version=export_version)
|
|
521
|
+
DR_content = os.sep.join([output_dir, DR_content])
|
|
522
|
+
VS_content = os.sep.join([output_dir, VS_content])
|
|
523
|
+
if force_retrieve or not(all(os.path.exists(filepath) for filepath in [DR_content, VS_content])):
|
|
524
|
+
if os.path.exists(DR_content):
|
|
525
|
+
os.remove(DR_content)
|
|
526
|
+
if os.path.exists(VS_content):
|
|
527
|
+
os.remove(VS_content)
|
|
528
|
+
if not(all(os.path.exists(filepath) for filepath in [DR_content, VS_content])):
|
|
529
|
+
content = dc.load(version, export=export_version, consolidate=use_consolidation)
|
|
530
|
+
data_request, vocabulary_server = transform_content(content, version)
|
|
531
|
+
write_json_output_file_content(DR_content, data_request)
|
|
532
|
+
write_json_output_file_content(VS_content, vocabulary_server)
|
|
533
|
+
return DR_content, VS_content
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
if __name__ == "__main__":
|
|
537
|
+
change_log_file(default=True)
|
|
538
|
+
change_log_level("debug")
|
|
539
|
+
logger = get_logger()
|
|
540
|
+
parser = argparse.ArgumentParser()
|
|
541
|
+
parser.add_argument("--input_file", default="dreq_raw_export.json",
|
|
542
|
+
help="Json file exported from airtable")
|
|
543
|
+
parser.add_argument("--output_files_template", default="request_basic_dump2.json",
|
|
544
|
+
help="Template to be used for output files")
|
|
545
|
+
parser.add_argument("--version", default="unknown", help="Version of the data used")
|
|
546
|
+
args = parser.parse_args()
|
|
547
|
+
content = read_json_input_file_content(args.input_file)
|
|
548
|
+
data_request, vocabulary_server = transform_content(content, args.version)
|
|
549
|
+
write_json_output_file_content("_".join(["DR", args.output_files_template]), data_request)
|
|
550
|
+
write_json_output_file_content("_".join(["VS", args.output_files_template]), vocabulary_server)
|
|
File without changes
|