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.
Files changed (36) hide show
  1. CMIP7_data_request_api-1.1.2.dist-info/LICENSE +21 -0
  2. CMIP7_data_request_api-1.1.2.dist-info/METADATA +210 -0
  3. CMIP7_data_request_api-1.1.2.dist-info/RECORD +36 -0
  4. CMIP7_data_request_api-1.1.2.dist-info/WHEEL +5 -0
  5. CMIP7_data_request_api-1.1.2.dist-info/entry_points.txt +2 -0
  6. CMIP7_data_request_api-1.1.2.dist-info/top_level.txt +1 -0
  7. data_request_api/__init__.py +1 -0
  8. data_request_api/command_line/__init__.py +0 -0
  9. data_request_api/command_line/export_dreq_lists_json.py +136 -0
  10. data_request_api/dev/JA/__init__.py +0 -0
  11. data_request_api/dev/JA/check_plev_requests.py +416 -0
  12. data_request_api/dev/JA/read_feedback_spreadsheet.py +141 -0
  13. data_request_api/dev/JA/workflow_example_GRtest.py +275 -0
  14. data_request_api/dev/JA/workflow_example_test.py +222 -0
  15. data_request_api/dev/MM/checksum.py +68 -0
  16. data_request_api/dev/MM/walking_data_request.ipynb +380 -0
  17. data_request_api/dev/MS/dreq_content_and_walking_data_request.ipynb +3755 -0
  18. data_request_api/dev/__init__.py +0 -0
  19. data_request_api/stable/__init__.py +0 -0
  20. data_request_api/stable/content/README.MD +106 -0
  21. data_request_api/stable/content/__init__.py +0 -0
  22. data_request_api/stable/content/dreq_api/__init__.py +0 -0
  23. data_request_api/stable/content/dreq_api/consolidate_export.py +488 -0
  24. data_request_api/stable/content/dreq_api/dreq_content.py +593 -0
  25. data_request_api/stable/content/dreq_api/mapping_table.py +335 -0
  26. data_request_api/stable/content/dreq_api/test_dreq_content.py +194 -0
  27. data_request_api/stable/content/dump_transformation.py +550 -0
  28. data_request_api/stable/query/__init__.py +0 -0
  29. data_request_api/stable/query/data_request.py +1120 -0
  30. data_request_api/stable/query/dreq_classes.py +372 -0
  31. data_request_api/stable/query/dreq_query.py +981 -0
  32. data_request_api/stable/query/vocabulary_server.py +208 -0
  33. data_request_api/stable/utilities/__init__.py +0 -0
  34. data_request_api/stable/utilities/logger.py +71 -0
  35. data_request_api/stable/utilities/tools.py +49 -0
  36. 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