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,981 @@
1
+ '''
2
+ Functions to extract information from the data request.
3
+ E.g., get variables requested for each experiment.
4
+
5
+ The module has two basic sections:
6
+
7
+ 1) Functions that take the data request content and convert it to python objects.
8
+ 2) Functions that interrogate the data request, usually using output from (1) as their input.
9
+
10
+ '''
11
+ import os
12
+ import hashlib
13
+ import json
14
+ from collections import OrderedDict
15
+
16
+
17
+ from data_request_api.stable.query.dreq_classes import (
18
+ dreq_table, expt_request, UNIQUE_VAR_NAME, PRIORITY_LEVELS)
19
+
20
+ # Version of data request content:
21
+ DREQ_VERSION = '' # if a tagged version is being used, set this in calling script
22
+
23
+ # Version of software (python API):
24
+ from data_request_api import version as api_version
25
+
26
+ ###############################################################################
27
+ # Functions to manage data request content input and use it to create python
28
+ # objects representing the tables.
29
+
30
+ def get_content_type(content):
31
+ '''
32
+ Internal function to distinguish the type of airtable export we are working with, based on the input dict.
33
+
34
+ Parameters
35
+ ----------
36
+ content : dict
37
+ Dict containing data request content exported from airtable.
38
+
39
+ Returns
40
+ -------
41
+ str indicating type of content:
42
+
43
+ 'working' : 3 bases containing the latest working version of data request content,
44
+ or 4 bases if the Schema table has been added to the export.
45
+
46
+ 'version' : 1 base containing the content of a tagged data request version.
47
+ '''
48
+ n = len(content)
49
+ if n in [3,4]:
50
+ content_type = 'working'
51
+ elif n == 1:
52
+ content_type = 'version'
53
+ else:
54
+ raise ValueError('Unable to determine type of data request content in the exported json file')
55
+ return content_type
56
+
57
+ def version_base_name():
58
+ return f'Data Request {DREQ_VERSION}'
59
+
60
+ def get_priority_levels():
61
+ '''
62
+ Return list of all valid priority levels (str) in the data request.
63
+ List is ordered from highest to lowest priority.
64
+ '''
65
+ priority_levels = [s.capitalize() for s in PRIORITY_LEVELS]
66
+
67
+ # The priorities are specified in PRIORITY_LEVELS from dreq_classes.
68
+ # Check here that 'Core' is highest priority.
69
+ # The 'Core' priority represents the Baseline Climate Variables (BCVs, https://doi.org/10.5194/egusphere-2024-2363).
70
+ # It should be highest priority unless something has been mistakenly modified in dreq_classes.py.
71
+ # Hence this check should NEVER fail, and is done here only to be EXTRA safe.
72
+ assert priority_levels[0] == 'Core', 'error in PRIORITY_LEVELS: highest priority should be Core (BCVs)'
73
+
74
+ return priority_levels
75
+
76
+ def get_table_id2name(base, base_name):
77
+ '''
78
+ Get a mapping from table id to table name
79
+ '''
80
+ table_id2name = {}
81
+ for table_name, table in base.items():
82
+ # assert table['name'] == table_name
83
+ # assert table['base_name'] == base_name, table['base_name'] + ', ' + base_name
84
+ table_id2name.update({
85
+ table['id'] : table['name']
86
+ })
87
+ assert len(table_id2name) == len(base), 'table ids are not unique!'
88
+ return table_id2name
89
+
90
+ def create_dreq_tables_for_request(content, consolidated=True):
91
+ '''
92
+ For the "request" part of the data request content (Opportunities, Variable Groups, etc),
93
+ render raw airtable export content as dreq_table objects.
94
+
95
+ For the "data" part of the data request, the corresponding function is create_dreq_tables_for_variables().
96
+
97
+ Parameters
98
+ ----------
99
+ content : dict
100
+ Raw airtable export. Dict is keyed by base name, for example:
101
+ {'Data Request Opportunities (Public)' : {
102
+ 'Opportunity' : {...},
103
+ ...
104
+ },
105
+ 'Data Request Variables (Public)' : {
106
+ 'Variables' : {...}
107
+ ...
108
+ }
109
+ }
110
+
111
+ Returns
112
+ -------
113
+ Dict whose keys are table names and values are dreq_table objects.
114
+ (The base name from the input 'content' dict no longer appears.)
115
+ '''
116
+ if not isinstance(content, dict):
117
+ raise TypeError('Input should be dict from raw airtable export json file')
118
+
119
+ # Content is dict loaded from raw airtable export json file
120
+ if consolidated:
121
+ base_name = 'Data Request'
122
+ content_type = 'consolidated'
123
+ else:
124
+ # for backward compatibility
125
+ content_type = get_content_type(content)
126
+ if content_type == 'working':
127
+ base_name = 'Data Request Opportunities (Public)'
128
+ elif content_type == 'version':
129
+ base_name = version_base_name()
130
+ else:
131
+ raise ValueError('Unknown content type: ' + content_type)
132
+ # base_name = 'Data Request'
133
+ base = content[base_name]
134
+
135
+ # Create objects representing data request tables
136
+ table_id2name = get_table_id2name(base, base_name)
137
+ for table_name, table in base.items():
138
+ # print('Creating table object for table: ' + table_name)
139
+ base[table_name] = dreq_table(table, table_id2name)
140
+
141
+ # Change names of tables if needed
142
+ # (insulates downstream code from upstream name changes that don't affect functionality)
143
+ change_table_names = {}
144
+ if content_type == 'working':
145
+ change_table_names = {
146
+ # old name : new name
147
+ 'Experiment' : 'Experiments',
148
+ 'Priority level' : 'Priority Level'
149
+ }
150
+ for old,new in change_table_names.items():
151
+ assert new not in base, 'New table name already exists: ' + new
152
+ if old not in base:
153
+ # print(f'Unavailable table {old}, skipping name change')
154
+ continue
155
+ base[new] = base[old]
156
+ base.pop(old)
157
+
158
+ # Make some adjustments that are specific to the Opportunity table
159
+ Opps = base['Opportunity']
160
+ Opps.rename_attr('title_of_opportunity', 'title') # rename title attribute for brevity in downstream code
161
+ for opp in Opps.records.values():
162
+ opp.title = opp.title.strip()
163
+ if content_type == 'working':
164
+ if 'variable_groups' not in Opps.attr2field:
165
+ # Try alternate names for the latest variable groups
166
+ try_vg_attr = []
167
+ try_vg_attr.append('working_updated_variable_groups') # takes precendence over originally requested groups
168
+ try_vg_attr.append('originally_requested_variable_groups')
169
+ for vg_attr in try_vg_attr:
170
+ if vg_attr in Opps.attr2field:
171
+ Opps.rename_attr(vg_attr, 'variable_groups')
172
+ break
173
+ assert 'variable_groups' in Opps.attr2field, f'unable to determine variable groups attribute for opportunity: {opp.title}'
174
+ exclude_opps = set()
175
+ for opp_id, opp in Opps.records.items():
176
+ if not hasattr(opp, 'experiment_groups'):
177
+ print(f' * WARNING * no experiment groups found for Opportunity: {opp.title}')
178
+ exclude_opps.add(opp_id)
179
+ if not hasattr(opp, 'variable_groups'):
180
+ print(f' * WARNING * no variable groups found for Opportunity: {opp.title}')
181
+ exclude_opps.add(opp_id)
182
+ if len(exclude_opps) > 0:
183
+ print('Quality control check is excluding these Opportunities:')
184
+ for opp_id in exclude_opps:
185
+ opp = Opps.records[opp_id]
186
+ print(f' {opp.title}')
187
+ Opps.delete_record(opp_id)
188
+ print()
189
+ if len(Opps.records) == 0:
190
+ # If there are no opportunities left, there's no point in continuing!
191
+ # This check is here because if something changes upstream in Airtable, it might cause
192
+ # the above code to erroneously remove all opportunities.
193
+ raise Exception(' * ERROR * All Opportunities were removed!')
194
+
195
+ return base
196
+
197
+ def create_dreq_tables_for_variables(content, consolidated=True):
198
+ '''
199
+ For the "data" part of the data request content (Variables, Cell Methods etc),
200
+ render raw airtable export content as dreq_table objects.
201
+
202
+ For the "request" part of the data request, the corresponding function is create_dreq_tables_for_request().
203
+
204
+ '''
205
+ if not isinstance(content, dict):
206
+ raise TypeError('Input should be dict from raw airtable export json file')
207
+
208
+ # Content is dict loaded from raw airtable export json file
209
+ if consolidated:
210
+ base_name = 'Data Request'
211
+ content_type = 'consolidated'
212
+ else:
213
+ # for backward compatibility
214
+ content_type = get_content_type(content)
215
+ if content_type == 'working':
216
+ base_name = 'Data Request Variables (Public)'
217
+ elif content_type == 'version':
218
+ base_name = version_base_name()
219
+ else:
220
+ raise ValueError('Unknown content type: ' + content_type)
221
+ base = content[base_name]
222
+
223
+ # Create objects representing data request tables
224
+ table_id2name = get_table_id2name(base, base_name)
225
+ for table_name, table in base.items():
226
+ # print('Creating table object for table: ' + table_name)
227
+ base[table_name] = dreq_table(table, table_id2name)
228
+
229
+ # Change names of tables if needed
230
+ # (insulates downstream code from upstream name changes that don't affect functionality)
231
+ change_table_names = {}
232
+ if content_type == 'working':
233
+ change_table_names = {
234
+ # old name : new name
235
+ 'Variable' : 'Variables',
236
+ 'Coordinate or Dimension' : 'Coordinates and Dimensions',
237
+ 'Physical Parameter' : 'Physical Parameters',
238
+ }
239
+ for old,new in change_table_names.items():
240
+ assert new not in base, 'New table name already exists: ' + new
241
+ base[new] = base[old]
242
+ base.pop(old)
243
+
244
+ return base
245
+
246
+ def _create_dreq_table_objects(content, working_base='Opportunities'):
247
+ '''
248
+ ******************
249
+ *** DEPRECATED ***
250
+ Replaced by two functions:
251
+ create_dreq_tables_for_request()
252
+ create_dreq_tables_for_variables()
253
+ ******************
254
+
255
+
256
+ Render raw airtable export content as dreq_table objects.
257
+
258
+ The exported content (input dict 'content') has a slightly different
259
+ structure depending on the content type, determined here by:
260
+ get_content_type(content)
261
+ If any finicky details need to be adjusted based on the content type,
262
+ this function handles them. For example, if the experiments table is
263
+ named "Experiments" in a versioned release but is named "Experiment"
264
+ in the 'working' content type. Ideally there would be no such differences,
265
+ but sometimes they happen. They are resolved here, insulating
266
+ downstream code from having to deal with them. That is, downstream code
267
+ should be independent of the content type.
268
+
269
+ Parameters
270
+ ----------
271
+ content : dict
272
+ Raw airtable export, keyed by base name:
273
+ { base 1 name : {
274
+ table 1 name : {...}
275
+ table 2 name : {...}
276
+ }
277
+ base 2 name : ...
278
+ }
279
+ For further details see "Structure of the exported content" in
280
+ scripts/README_airtable_export.md in the content repo:
281
+ https://github.com/CMIP-Data-Request/CMIP7_DReq_Content/
282
+ or equivalently for release versions:
283
+ https://github.com/CMIP-CMIP/CMIP7_DReq_Content/
284
+
285
+ working_base : str
286
+ If content dict has more than one base, as for the "working version",
287
+ this specifies which one to convert and return.
288
+
289
+ Returns
290
+ -------
291
+ base : dict
292
+ Dict keys are table names, values are dreq_table objects.
293
+ '''
294
+ if not isinstance(content, dict):
295
+ raise TypeError('Input should be dict from raw airtable export json file')
296
+
297
+ # Content is dict loaded from raw airtable export json file
298
+ content_type = get_content_type(content)
299
+
300
+ if content_type == 'working':
301
+ if working_base == 'Opportunities':
302
+ base_name = 'Data Request Opportunities (Public)'
303
+ elif working_base == 'Variables':
304
+ base_name = 'Data Request Variables (Public)'
305
+ else:
306
+ raise ValueError('Which working base to use? Unknown type: ' + working_base)
307
+ elif content_type == 'version':
308
+ base_name = version_base_name()
309
+ else:
310
+ raise ValueError('Unknown content type: ' + content_type)
311
+ base = content[base_name]
312
+
313
+ # Get a mapping from table id to table name
314
+ table_id2name = {}
315
+ for table_name, table in base.items():
316
+ assert table['name'] == table_name
317
+ assert table['base_name'] == base_name
318
+ table_id2name.update({
319
+ table['id'] : table['name']
320
+ })
321
+ assert len(table_id2name) == len(base)
322
+ # Create objects representing data request tables
323
+ for table_name, table in base.items():
324
+ # print('Creating table object for table: ' + table_name)
325
+ base[table_name] = dreq_table(table, table_id2name)
326
+
327
+ if 'Opportunity' in base and working_base == 'Opportunities':
328
+ # Make some adjustments that are specific to the Opportunity table
329
+ Opps = base['Opportunity']
330
+ Opps.rename_attr('title_of_opportunity', 'title') # rename title attribute for brevity in downstream code
331
+ if content_type == 'working':
332
+ if 'variable_groups' not in Opps.attr2field:
333
+ if 'originally_requested_variable_groups' in Opps.attr2field:
334
+ Opps.rename_attr('originally_requested_variable_groups', 'variable_groups')
335
+ exclude_opps = set()
336
+ for opp_id, opp in Opps.records.items():
337
+ if not hasattr(opp, 'experiment_groups'):
338
+ print(f' * WARNING * no experiment groups found for Opportunity {opp.title}')
339
+ exclude_opps.add(opp_id)
340
+ if not hasattr(opp, 'variable_groups'):
341
+ print(f' * WARNING * no variable groups found for Opportunity {opp.title}')
342
+ exclude_opps.add(opp_id)
343
+ if len(exclude_opps) > 0:
344
+ print('Excluding Opportunities:')
345
+ for opp_id in exclude_opps:
346
+ opp = Opps.records[opp_id]
347
+ print(f' {opp.title}')
348
+ Opps.delete_record(opp_id)
349
+ if len(Opps.records) == 0:
350
+ # If there are no opportunities left, there's no point in continuing!
351
+ # This check is here because if something changes upstream in Airtable, it might cause
352
+ # the above code to erroneously remove all opportunities.
353
+ raise Exception(' * ERROR * All Opportunities were removed!')
354
+
355
+ # Other adjustments
356
+ if content_type == 'working':
357
+
358
+ if working_base == 'Opportunities':
359
+ change_table_names = {
360
+ # old name : new name
361
+ 'Experiment' : 'Experiments',
362
+ }
363
+
364
+ # if 'Experiments' not in base:
365
+ # # Unfortunately the 'working' bases have a different table name for experiments
366
+ # # than the official releases (as of Oct 2024)
367
+ # base['Experiments'] = base['Experiment']
368
+ # base.pop('Experiment')
369
+ # assert 'Experiment' not in base
370
+
371
+ elif working_base == 'Variables':
372
+ change_table_names = {
373
+ # old name : new name
374
+ 'Variable' : 'Variables',
375
+ 'Coordinate or Dimension' : 'Coordinates and Dimensions',
376
+ 'Physical Parameter' : 'Physical Parameters',
377
+ }
378
+
379
+ # if 'Variables' not in base:
380
+ # base['Variables'] = base['Variable']
381
+ # base.pop('Variable')
382
+ # assert 'Variable' not in base
383
+
384
+ for old,new in change_table_names.items():
385
+ assert new not in base, 'New table name already exists: ' + new
386
+ base[new] = base[old]
387
+ base.pop(old)
388
+
389
+ return base
390
+
391
+ ###############################################################################
392
+ # Functions to interrogate the data request, e.g. get variables requested for
393
+ # each experiment.
394
+
395
+ def get_opp_ids(use_opps, Opps, verbose=False, quality_control=True):
396
+ '''
397
+ Return list of unique opportunity identifiers.
398
+
399
+ Parameters
400
+ ----------
401
+ use_opps : str or list
402
+ "all" : return all available ids
403
+ list of str : return ids for with the listed opportunity titles
404
+ Opps : dreq_table
405
+ table object representing the opportunities table
406
+ '''
407
+ opp_ids = []
408
+ records = Opps.records
409
+ if use_opps == 'all':
410
+ # Include all opportunities
411
+ opp_ids = list(records.keys())
412
+ elif isinstance(use_opps, list):
413
+ use_opps = sorted(set(use_opps))
414
+ if all([isinstance(s, str) for s in use_opps]):
415
+ # opp_ids = [opp_id for opp_id,opp in records.items() if opp.title in use_opps]
416
+ title2id = {opp.title : opp_id for opp_id,opp in records.items()}
417
+ assert len(records) == len(title2id), 'Opportunity titles are not unique'
418
+ for title in use_opps:
419
+ if title in title2id:
420
+ opp_ids.append(title2id[title])
421
+ else:
422
+ # print(f'\n* WARNING * Opportunity not found: {title}\n')
423
+ raise Exception(f'\n* ERROR * The specified Opportunity is not found: {title}\n')
424
+
425
+ assert len(set(opp_ids)) == len(opp_ids), 'found repeated opportunity ids'
426
+
427
+ if quality_control:
428
+ valid_opp_status = ['Accepted', 'Under review']
429
+ discard_opp_id = set()
430
+ for opp_id in opp_ids:
431
+ opp = Opps.get_record(opp_id)
432
+ # print(opp)
433
+ # if len(opp) == 0:
434
+ # # discard empty opportunities
435
+ # discard_opp_id.add(opp_id)
436
+ if hasattr(opp, 'status') and opp.status not in valid_opp_status:
437
+ discard_opp_id.add(opp_id)
438
+ for opp_id in discard_opp_id:
439
+ Opps.delete_record(opp_id)
440
+ opp_ids.remove(opp_id)
441
+ del discard_opp_id
442
+
443
+ if verbose:
444
+ if len(opp_ids) > 0:
445
+ print('Found {} Opportunities:'.format(len(opp_ids)))
446
+ for opp_id in opp_ids:
447
+ opp = records[opp_id]
448
+ print(' ' + opp.title)
449
+ else:
450
+ print('No Opportunities found')
451
+
452
+ return opp_ids
453
+
454
+ def get_var_group_priority(var_group, PriorityLevel=None):
455
+ '''
456
+ Returns string stating the priorty level of variable group.
457
+
458
+ Parameters
459
+ ----------
460
+ var_group : dreq_record
461
+ Object representing a variable group
462
+ Its "priority_level" attribute specifies the priority as either string or link to PriorityLevel table
463
+ PriorityLevel : dreq_table
464
+ Required if var_group.priority_level is link to PriorityLevel table
465
+
466
+ Returns
467
+ -------
468
+ str that states the priority level, e.g. "High"
469
+ '''
470
+ if not hasattr(var_group, 'priority_level'):
471
+ return 'Undefined'
472
+
473
+ if isinstance(var_group.priority_level, list):
474
+ assert len(var_group.priority_level) == 1, 'Variable group should have one specified priority level'
475
+ link = var_group.priority_level[0]
476
+ assert isinstance(PriorityLevel, dreq_table)
477
+ rec = PriorityLevel.records[link.record_id]
478
+ priority_level = rec.name
479
+ elif isinstance(var_group.priority_level, str):
480
+ priority_level = var_group.priority_level
481
+ else:
482
+ raise Exception('Unable to determine variable group priority level')
483
+ if not isinstance(priority_level, str):
484
+ raise TypeError('Priority level should be str, instead got {}'.format(type(priority_level)))
485
+ return priority_level
486
+
487
+ def get_unique_var_name(var):
488
+ '''
489
+ Return name that uniquely identifies a variable.
490
+ Reason to make this a function is to control this choice in one place.
491
+ E.g., if compound_name is used initially, but something else chosen later.
492
+
493
+ Parameters
494
+ ----------
495
+ var : dreq_record
496
+ Object representing a variable
497
+
498
+ Returns
499
+ -------
500
+ str that uniquely identifes a variable in the data request
501
+ '''
502
+ if UNIQUE_VAR_NAME == 'compound name':
503
+ return var.compound_name
504
+ else:
505
+ raise ValueError('Unknown identifier for UNIQUE_VAR_NAME: ' + UNIQUE_VAR_NAME +
506
+ '\nHow should the unique variable name be determined?')
507
+
508
+ def get_opp_expts(opp, ExptGroups, Expts, verbose=False):
509
+ '''
510
+ For one Opportunity, get its requested experiments.
511
+ Input parameters are not modified.
512
+
513
+ Parameters
514
+ ----------
515
+ opp : dreq_record
516
+ One record from the Opportunity table
517
+ ExptGroups : dreq_table
518
+ Experiment Group table
519
+ Expts : dreq_table
520
+ Experiments table
521
+
522
+ Returns
523
+ -------
524
+ Set giving names of experiments from which the Opportunity requests output.
525
+ Example: {'historical', 'piControl'}
526
+ '''
527
+ # Follow links to experiment groups to find the names of requested experiments
528
+ opp_expts = set() # list to store names of experiments requested by this Opportunity
529
+ if verbose:
530
+ print(' Experiment Groups ({}):'.format(len(opp.experiment_groups)))
531
+ for link in opp.experiment_groups:
532
+ # expt_group = base[link.table_name].records[link.record_id]
533
+ expt_group = ExptGroups.records[link.record_id]
534
+
535
+ if not hasattr(expt_group, 'experiments'):
536
+ continue
537
+
538
+ if verbose:
539
+ print(f' {expt_group.name} ({len(expt_group.experiments)} experiments)')
540
+
541
+ for link in expt_group.experiments:
542
+ expt = Expts.records[link.record_id]
543
+ # print(f' {expt.experiment}')
544
+ opp_expts.add(expt.experiment)
545
+ return opp_expts
546
+
547
+ def get_opp_vars(opp, priority_levels, VarGroups, Vars, PriorityLevel=None, verbose=False):
548
+ '''
549
+ For one Opportunity, get its requested variables grouped by priority level.
550
+ Input parameters are not modified.
551
+
552
+ Parameters
553
+ ----------
554
+ opp : dreq_record
555
+ One record from the Opportunity table
556
+ priority_levels : list[str]
557
+ Priority levels to get, example: ['High', 'Medium']
558
+ VarGroups : dreq_table
559
+ Variable Group table
560
+ Vars : dreq_table
561
+ Variables table
562
+ PriorityLevel : dreq_table
563
+ Required if var_group.priority_level is link to PriorityLevel table
564
+
565
+ Returns
566
+ -------
567
+ Dict giving set of variables requested at each specified priority level
568
+ Example: {'High' : {'Amon.tas', 'day.tas'}, 'Medium' : {'day.ua'}}
569
+ '''
570
+ # Follow links to variable groups to find names of requested variables
571
+ opp_vars = {p : set() for p in priority_levels}
572
+ if verbose:
573
+ print(' Variable Groups ({}):'.format(len(opp.variable_groups)))
574
+ for link in opp.variable_groups:
575
+ var_group = VarGroups.records[link.record_id]
576
+
577
+ priority_level = get_var_group_priority(var_group, PriorityLevel)
578
+ if priority_level not in priority_levels:
579
+ continue
580
+
581
+ if verbose:
582
+ print(f' {var_group.name} ({len(var_group.variables)} variables, {priority_level} priority)')
583
+
584
+ for link in var_group.variables:
585
+ var = Vars.records[link.record_id]
586
+ var_name = get_unique_var_name(var)
587
+ # Add this variable to the list of requested variables at the specified priority
588
+ opp_vars[priority_level].add(var_name)
589
+ return opp_vars
590
+
591
+
592
+
593
+ def get_requested_variables(content, use_opps='all', priority_cutoff='Low', verbose=True, consolidated=True, check_core_variables=True):
594
+ '''
595
+ Return variables requested for each experiment, as a function of opportunities supported and priority level of variables.
596
+
597
+ Parameters
598
+ ----------
599
+ content : dict
600
+ Dict containing either:
601
+ - data request content as exported from airtable
602
+ OR
603
+ - dreq_table objects representing tables (dict keys are table names)
604
+ use_opp : str or list of str/int
605
+ Identifies the opportunities being supported. Options:
606
+ 'all' : include all available opportunities
607
+ integers : include opportunities identified by their integer IDs
608
+ strings : include opportunities identified by their titles
609
+ priority_cutoff : str
610
+ Only return variables of equal or higher priority level than priority_cutoff.
611
+ E.g., priority_cutoff='Low' means all priority levels are returned.
612
+ check_core_variables : bool
613
+ True ==> check that all experiments contain a non-empty list of Core variables,
614
+ and that it's the same list for all experiments.
615
+
616
+ Returns
617
+ -------
618
+ Dict keyed by experiment name, giving prioritized variables for each experiment.
619
+ Example:
620
+ { 'Header' : ... (Header contains info about where this request comes from)
621
+ 'experiment' : {
622
+ 'historical' :
623
+ 'High' : ['Amon.tas', 'day.tas', ...],
624
+ 'Medium' : ...
625
+ }
626
+ ...
627
+ }
628
+ }
629
+ '''
630
+ if isinstance(content, dict):
631
+ if all([isinstance(table, dreq_table) for table in content.values()]):
632
+ # tables have already been rendered as dreq_table objects
633
+ base = content
634
+ else:
635
+ # render tables as dreq_table objects
636
+ base = create_dreq_tables_for_request(content, consolidated=consolidated)
637
+ else:
638
+ raise TypeError('Expect dict as input')
639
+
640
+ Opps = base['Opportunity']
641
+ opp_ids = get_opp_ids(use_opps, Opps, verbose=verbose)
642
+
643
+ ExptGroups = base['Experiment Group']
644
+ Expts = base['Experiments']
645
+ VarGroups = base['Variable Group']
646
+ Vars = base['Variables']
647
+
648
+ # all_priority_levels = ['Core', 'High', 'Medium', 'Low']
649
+ # all_priority_levels = [s.capitalize() for s in PRIORITY_LEVELS]
650
+ all_priority_levels = get_priority_levels()
651
+
652
+ if 'Priority Level' in base:
653
+ PriorityLevel = base['Priority Level']
654
+ priority_levels_from_table = [rec.name for rec in PriorityLevel.records.values()]
655
+ assert set(all_priority_levels) == set(priority_levels_from_table), \
656
+ 'inconsistent priority levels:\n ' + str(all_priority_levels) + '\n ' + str(priority_levels_from_table)
657
+ else:
658
+ PriorityLevel = None
659
+ priority_cutoff = priority_cutoff.capitalize()
660
+ if priority_cutoff not in all_priority_levels:
661
+ raise ValueError('Invalid priority level cutoff: ' + priority_cutoff + '\nCould not determine priority levels to include.')
662
+ m = all_priority_levels.index(priority_cutoff)
663
+ priority_levels = all_priority_levels[:m+1]
664
+ del priority_cutoff
665
+
666
+ # Loop over Opportunities to get prioritized lists of variables
667
+ request = {} # dict to hold aggregated request
668
+ for opp_id in opp_ids:
669
+ opp = Opps.records[opp_id] # one record from the Opportunity table
670
+
671
+ if verbose:
672
+ print(f'Opportunity: {opp.title}')
673
+
674
+ opp_expts = get_opp_expts(opp, ExptGroups, Expts, verbose=verbose)
675
+ opp_vars = get_opp_vars(opp, priority_levels, VarGroups, Vars, PriorityLevel, verbose=verbose)
676
+
677
+ # Aggregate this Opportunity's request into the master list of requests
678
+ for expt_name in opp_expts:
679
+ if expt_name not in request:
680
+ # If we haven't encountered this experiment yet, initialize an expt_request object for it
681
+ request[expt_name] = expt_request(expt_name)
682
+
683
+ # Add this Opportunity's variables request to the expt_request object
684
+ for priority_level, var_names in opp_vars.items():
685
+ request[expt_name].add_vars(var_names, priority_level)
686
+
687
+ opp_titles = sorted([Opps.get_record(opp_id).title for opp_id in opp_ids])
688
+ requested_vars = {
689
+ 'Header' : {
690
+ 'Opportunities' : opp_titles,
691
+ 'dreq version' : DREQ_VERSION,
692
+ },
693
+ 'experiment' : {},
694
+ }
695
+ for expt_name, expt_req in request.items():
696
+ requested_vars['experiment'].update(expt_req.to_dict())
697
+
698
+ if check_core_variables:
699
+ # Confirm that 'Core' priority level variables are included, and identical for each experiment.
700
+ # The setting of priority_levels list, above, should guarantee this.
701
+ # Putting this extra check here just to be extra sure.
702
+ core_vars = set()
703
+ for expt_name, expt_req in requested_vars['experiment'].items():
704
+ assert 'Core' in expt_req, 'Missing Core variables for experiment: ' + expt_name
705
+ vars = set(expt_req['Core'])
706
+ assert len(vars) > 0, 'Empty Core variables list for experiment: ' + expt_name
707
+ if len(core_vars) == 0:
708
+ core_vars = vars
709
+ assert vars == core_vars, 'Inconsistent Core variables for experiment: ' + expt_name
710
+
711
+ return requested_vars
712
+
713
+
714
+
715
+ def _get_requested_variables(content, use_opp='all', priority_cutoff='Low', verbose=True):
716
+ '''
717
+ ******************
718
+ *** DEPRECATED ***
719
+ This is an initial version of the search function that uses python dicts from the airtable export directly.
720
+ It may be useful to keep in the module for testing, i.e. to validate other codes.
721
+ ******************
722
+
723
+ Return variables requested for each experiment, as a function of opportunities supported and priority level of variables.
724
+
725
+ Parameters
726
+ ----------
727
+ content : dict
728
+ Dict containing data request content exported from airtable.
729
+ use_opp : str or list of str/int
730
+ Identifies the opportunities being supported. Options:
731
+ 'all' : include all available opportunities
732
+ integers : include opportunities identified by their integer IDs
733
+ strings : include opportunities identified by their titles
734
+ priority_cutoff : str
735
+ Only return variables of equal or higher priority level than priority_cutoff.
736
+ E.g., priority_cutoff='Low' means all priority levels are returned.
737
+
738
+ Returns
739
+ -------
740
+ Dict keyed by experiment name, giving prioritized variables for each experiment.
741
+ Example:
742
+ { 'Header' : ... (Header contains info about where this request comes from)
743
+ 'experiment' : {
744
+ 'historical' :
745
+ 'High' : ['Amon.tas', 'day.tas', ...],
746
+ 'Medium' : ...
747
+ }
748
+ ...
749
+ }
750
+ }
751
+ '''
752
+
753
+ if not isinstance(content, dict):
754
+ raise TypeError('Input should be dict from raw airtable export json file')
755
+
756
+ content_type = get_content_type(content)
757
+ if content_type == 'working':
758
+ base_name = 'Data Request Opportunities (Public)'
759
+ elif content_type == 'version':
760
+ base_name = version_base_name()
761
+
762
+ tables = content[base_name]
763
+
764
+ all_opps = tables['Opportunity'] # Opportunities table, specifying all defined data request opportunities
765
+
766
+ filter_opps = True
767
+ if filter_opps:
768
+ # somehow empty opportunities are in the v1.0alpha base
769
+ # this will cause problems below
770
+ # discard them
771
+ discard_opp_id = set()
772
+ for opp_id, opp in all_opps['records'].items():
773
+ if len(opp) == 0:
774
+ # discard empty opportunities
775
+ discard_opp_id.add(opp_id)
776
+ if 'Status' in opp and opp['Status'] not in ['Accepted', 'Under review']:
777
+ discard_opp_id.add(opp_id)
778
+ for opp_id in discard_opp_id:
779
+ all_opps['records'].pop(opp_id)
780
+ del discard_opp_id
781
+
782
+ if use_opp == 'all':
783
+ # Include all opportunities
784
+ use_opp = [opp_id for opp_id in all_opps['records']]
785
+ elif isinstance(use_opp, list):
786
+ if all([isinstance(m, int) for m in use_opp]):
787
+ # Opportunity IDs have been given as input
788
+ use_opp = [opp_id for opp_id,opp in all_opps['records'].items() if int(opp['Opportunity ID']) in use_opp]
789
+ elif all([isinstance(s, str) for s in use_opp]):
790
+ # Opportunity titles have been given as input
791
+ use_opp = [opp_id for opp_id,opp in all_opps['records'].items() if opp['Title of Opportunity'] in use_opp]
792
+ use_opp = list(set(use_opp))
793
+ if len(use_opp) == 0:
794
+ print('No opportunities found')
795
+ return
796
+ if verbose:
797
+ n = len(use_opp)
798
+ print(f'Finding requested variables for {n} Opportunities:')
799
+ for opp_id in use_opp:
800
+ opp = all_opps['records'][opp_id]
801
+ print(' ' + opp['Title of Opportunity'])
802
+
803
+ # Loop over the opportunities
804
+ expt_vars = {}
805
+ priority_levels = get_priority_levels()
806
+ for opp_id in use_opp:
807
+ opp = all_opps['records'][opp_id] # one record from the Opportunity table
808
+
809
+ if 'Experiment Groups' not in opp:
810
+ print('No experiment groups defined for opportunity: ' + opp['Title of Opportunity'])
811
+ continue
812
+ opp_expts = set() # will hold names of experiments requested by this opportunity
813
+ for expt_group_id in opp['Experiment Groups']: # Loop over experiment groups in this opportunity
814
+ expt_group = tables['Experiment Group']['records'][expt_group_id]
815
+ # Get names of experiments in this experiment group
816
+ for expt_id in expt_group['Experiments']:
817
+
818
+ # cluge
819
+ if content_type == 'working':
820
+ expt_table_name = 'Experiment'
821
+ elif content_type == 'version':
822
+ expt_table_name = 'Experiments'
823
+
824
+ expt = tables[expt_table_name]['records'][expt_id]
825
+ expt_key = expt[' Experiment'].strip() # Name of experiment, e.g "historical"
826
+ opp_expts.add(expt_key)
827
+ if expt_key not in expt_vars:
828
+ expt_vars[expt_key] = {p : set() for p in priority_levels}
829
+
830
+ try_vg_fields = []
831
+ try_vg_fields.append('Variable Groups')
832
+ try_vg_fields.append('Working/Updated Variable Groups')
833
+ try_vg_fields.append('Originally Requested Variable Groups')
834
+ vg_key = None
835
+ for vg_key in try_vg_fields:
836
+ if vg_key in opp:
837
+ break
838
+ if vg_key not in opp:
839
+ print('No variable groups defined for opportunity: ' + opp['Title of Opportunity'])
840
+ continue
841
+ for var_group_id in opp[vg_key]: # Loop over variable groups in this opportunity
842
+ var_group = tables['Variable Group']['records'][var_group_id]
843
+ priority = var_group['Priority Level']
844
+
845
+ if isinstance(priority, list): # True if priority is a link to a Priority Level record (instead of just a string)
846
+ assert len(priority) == 1, 'Variable Group should have one specified priority level'
847
+ prilev_id = priority[0]
848
+
849
+ # prilev = tables['Priority Level']['records'][prilev_id]
850
+ # cluge for testing with latest working bases
851
+ pl_table = None
852
+ pl_try = ['Priority Level', 'Priority level']
853
+ for s in pl_try:
854
+ if s in tables:
855
+ pl_table = tables[s]
856
+ break
857
+ prilev = pl_table['records'][prilev_id]
858
+ priority = prilev['Name']
859
+ assert priority in priority_levels, 'Unrecognized priority level: ' + priority
860
+ del prilev
861
+
862
+ # Get names of variables in this variable group
863
+ for var_id in var_group['Variables']: # Loop over variables in this variable group
864
+ var = tables['Variables']['records'][var_id]
865
+ var_key = var['Compound Name'] # Name of variable, e.g. "Amon.tas"
866
+ for expt_key in opp_expts:
867
+ # Add this variable to the experiment's output set, at the priority level specified by the variable group
868
+ expt_vars[expt_key][priority].add(var_key)
869
+
870
+ # Remove overlaps between priority levels
871
+ assert priority_levels == ['Core', 'High', 'Medium', 'Low']
872
+ for expt_key, expt_var in expt_vars.items():
873
+ # remove any Core priority variables from other groups
874
+ for p in ['High', 'Medium', 'Low']:
875
+ expt_var[p] = expt_var[p].difference(expt_var['Core'])
876
+ # remove any High priority variables from lower priority groups
877
+ for p in ['Medium', 'Low']:
878
+ expt_var[p] = expt_var[p].difference(expt_var['High'])
879
+ # remove any Medium priority variables from lower priority groups
880
+ for p in ['Low']:
881
+ expt_var[p] = expt_var[p].difference(expt_var['Medium'])
882
+ # Remove unwanted priority levels
883
+ for expt_key, expt_var in expt_vars.items():
884
+ if priority_cutoff.lower() == 'core':
885
+ expt_var.pop('High')
886
+ expt_var.pop('Medium')
887
+ expt_var.pop('Low')
888
+ elif priority_cutoff.lower() == 'high':
889
+ expt_var.pop('Medium')
890
+ expt_var.pop('Low')
891
+ elif priority_cutoff.lower() == 'medium':
892
+ expt_var.pop('Low')
893
+
894
+ for expt, req in expt_vars.items():
895
+ # Change sets to lists
896
+ for p in req:
897
+ req[p] = sorted(req[p], key=str.lower)
898
+
899
+ opp_titles = sorted([all_opps['records'][opp_id]['Title of Opportunity'] for opp_id in use_opp])
900
+ requested_vars = {
901
+ 'Header' : {
902
+ 'Opportunities' : opp_titles,
903
+ 'dreq version' : DREQ_VERSION,
904
+ },
905
+ 'experiment' : expt_vars,
906
+ }
907
+ return requested_vars
908
+
909
+
910
+ def show_requested_vars_summary(expt_vars, use_dreq_version):
911
+ '''
912
+ Display quick summary to stdout of variables requested.
913
+ expt_vars is the output dict from dq.get_requested_variables().
914
+ '''
915
+ print(f'\nFor data request version {use_dreq_version}, number of requested variables found by experiment:')
916
+ priority_levels=get_priority_levels()
917
+ for expt, req in sorted(expt_vars['experiment'].items()):
918
+ d = {p : 0 for p in priority_levels}
919
+ for p in priority_levels:
920
+ if p in req:
921
+ d[p] = len(req[p])
922
+ n_total = sum(d.values())
923
+ print(f' {expt} : ' + ' ,'.join(['{p}={n}'.format(p=p,n=d[p]) for p in priority_levels]) + f', TOTAL={n_total}')
924
+
925
+
926
+ def write_requested_vars_json(outfile, expt_vars, use_dreq_version, priority_cutoff, content_path):
927
+ '''
928
+ Write a nicely formatted json file with lists of requested variables by experiment.
929
+ expt_vars is the output dict from dq.get_requested_variables().
930
+ '''
931
+
932
+ Header = OrderedDict({
933
+ 'Description' : 'This file gives the names of output variables that are requested from CMIP experiments by the supported Opportunities. The variables requested from each experiment are listed under each experiment name, grouped according to the priority level at which they are requested. For each experiment, the prioritized list of variables was determined by compiling together all requests made by the supported Opportunities for output from that experiment.',
934
+ 'Opportunities supported' : sorted(expt_vars['Header']['Opportunities'], key=str.lower)
935
+ })
936
+
937
+ # List supported priority levels
938
+ priority_levels=get_priority_levels()
939
+ priority_cutoff = priority_cutoff.capitalize()
940
+ m = priority_levels.index(priority_cutoff)+1
941
+ Header.update({
942
+ 'Priority levels supported' : priority_levels[:m]
943
+ })
944
+ for req in expt_vars['experiment'].values():
945
+ for p in priority_levels[m:]:
946
+ assert req[p] == []
947
+ req.pop(p) # remove empty lists of unsupported priorities from the output
948
+
949
+ # List included experiments
950
+ Header.update({
951
+ 'Experiments included' : sorted(expt_vars['experiment'].keys(), key=str.lower)
952
+ })
953
+
954
+ # Get provenance of content to include in the Header
955
+ # content_path = dc._dreq_content_loaded['json_path']
956
+ with open(content_path, 'rb') as f:
957
+ content_hash = hashlib.sha256(f.read()).hexdigest()
958
+ Header.update({
959
+ 'dreq content version' : use_dreq_version,
960
+ 'dreq content file' : os.path.basename(os.path.normpath(content_path)),
961
+ 'dreq content sha256 hash' : content_hash,
962
+ 'dreq api version' : api_version,
963
+ })
964
+
965
+ out = {
966
+ 'Header' : Header,
967
+ 'experiment' : OrderedDict(),
968
+ }
969
+ expt_names = sorted(expt_vars['experiment'].keys(), key=str.lower)
970
+ for expt_name in expt_names:
971
+ out['experiment'][expt_name] = OrderedDict()
972
+ req = expt_vars['experiment'][expt_name]
973
+ for p in priority_levels:
974
+ if p in req:
975
+ out['experiment'][expt_name][p] = req[p]
976
+
977
+ # Write the results to json
978
+ with open(outfile, 'w') as f:
979
+ # json.dump(expt_vars, f, indent=4, sort_keys=True)
980
+ json.dump(out, f, indent=4)
981
+ print('\nWrote requested variables to ' + outfile)