From c1643869add13708102d778a49e3e6099299614b Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 3 Apr 2019 09:57:09 +0200 Subject: [PATCH 01/14] Correct typo --- pyessv/__init__.py | 4 ++-- pyessv/_parsers/__init__.py | 6 +++--- pyessv/_ws/handlers/validate_identifier.py | 2 +- pyessv/_ws/handlers/validate_identifier_set.py | 2 +- tests/test_interface.py | 4 ++-- tests/test_parse_identifiers.py | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyessv/__init__.py b/pyessv/__init__.py index 2803387..230d71c 100644 --- a/pyessv/__init__.py +++ b/pyessv/__init__.py @@ -72,8 +72,8 @@ from pyessv._model import Term from pyessv._parser import parse -from pyessv._parsers import parse_dataset_identifer -from pyessv._parsers import parse_dataset_identifers +from pyessv._parsers import parse_dataset_identifier +from pyessv._parsers import parse_dataset_identifiers from pyessv._utils.logger import log from pyessv._utils.logger import log_error diff --git a/pyessv/_parsers/__init__.py b/pyessv/_parsers/__init__.py index ccca2f0..a836502 100644 --- a/pyessv/_parsers/__init__.py +++ b/pyessv/_parsers/__init__.py @@ -30,7 +30,7 @@ } -def parse_dataset_identifers(project, identifiers): +def parse_dataset_identifiers(project, identifiers): """Parses a collection of dataset identifiers. :param str project: Project code. @@ -44,12 +44,12 @@ def parse_dataset_identifers(project, identifiers): result = set() for identifier in identifiers: - result = result.union(parse_dataset_identifer(project, identifier)) + result = result.union(parse_dataset_identifier(project, identifier)) return result -def parse_dataset_identifer(project, identifier): +def parse_dataset_identifier(project, identifier): """Parses a dataset identifier. :param str project: Project code. diff --git a/pyessv/_ws/handlers/validate_identifier.py b/pyessv/_ws/handlers/validate_identifier.py index b6ea640..3e0cc1a 100644 --- a/pyessv/_ws/handlers/validate_identifier.py +++ b/pyessv/_ws/handlers/validate_identifier.py @@ -26,7 +26,7 @@ # Map of identifier type to parser. _PARSERS = { - 'dataset': pyessv.parse_dataset_identifer, + 'dataset': pyessv.parse_dataset_identifier, } diff --git a/pyessv/_ws/handlers/validate_identifier_set.py b/pyessv/_ws/handlers/validate_identifier_set.py index 37365f5..811c5af 100644 --- a/pyessv/_ws/handlers/validate_identifier_set.py +++ b/pyessv/_ws/handlers/validate_identifier_set.py @@ -28,7 +28,7 @@ # Map of identifier type to parser. _PARSERS = { - 'dataset': pyessv.parse_dataset_identifers, + 'dataset': pyessv.parse_dataset_identifiers, } diff --git a/tests/test_interface.py b/tests/test_interface.py index b3ad69c..d090a76 100644 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -85,8 +85,8 @@ 'log_warning', # ... parsing 'parse', - 'parse_dataset_identifer', - 'parse_dataset_identifers', + 'parse_dataset_identifier', + 'parse_dataset_identifiers', # ... validation 'get_errors', 'is_valid', diff --git a/tests/test_parse_identifiers.py b/tests/test_parse_identifiers.py index 626f666..adada98 100644 --- a/tests/test_parse_identifiers.py +++ b/tests/test_parse_identifiers.py @@ -21,14 +21,14 @@ # Test configuration: project, parsing function, template seperator, strictness, identifiers. _CONFIG = { - ('cmip5', LIB.parse_dataset_identifer, '.', ( + ('cmip5', LIB.parse_dataset_identifier, '.', ( 'cmip5.output1.IPSL.IPSL-CM5A-LR.aqua4K.3hr.atmos.3hr.r2i1p1', 'cmip5.output2.IPSL.IPSL-CM5A-LR.historicalMisc.mon.ocean.Omon.r2i1p1' )), - ('cmip6', LIB.parse_dataset_identifer, '.', ( + ('cmip6', LIB.parse_dataset_identifier, '.', ( 'cmip6.FAFMIP.IPSL.IPSL-CM6A-LR.amip.r1i1p1f1.Amon.abs550aer.gm', )), - ('cordex', LIB.parse_dataset_identifer, '.', ( + ('cordex', LIB.parse_dataset_identifier, '.', ( 'cordex.output.AFR-44.MOHC.MOHC-HadGEM2-ES.rcp60.r12i1p1.HadGEM3-RA.v1.mon.areacella', 'cordex.output.EUR-11.SMHI.ICHEC-EC-EARTH.rcp85.r12i1p1.RCA4.v1.sem.rsdt' )), From a89dd4912b2974075e1a537290af7a5c3a566b20 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 3 Apr 2019 10:52:33 +0200 Subject: [PATCH 02/14] Add version collection to project dataset parsers --- pyessv/_parsers/cmip5_dataset_id.py | 12 ++++++------ pyessv/_parsers/cmip6_dataset_id.py | 10 +++++----- pyessv/_parsers/cordex_dataset_id.py | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pyessv/_parsers/cmip5_dataset_id.py b/pyessv/_parsers/cmip5_dataset_id.py index 74ad009..36c3fa0 100644 --- a/pyessv/_parsers/cmip5_dataset_id.py +++ b/pyessv/_parsers/cmip5_dataset_id.py @@ -22,7 +22,7 @@ _TEST_IDENTIFIER = 'cmip5.output2.IPSL.IPSL-CM5A-LR.historicalMisc.mon.ocean.Omon.r2i1p1.v20150504' # Template that identifiers must conform to. -_TEMPLATE = 'cmip5.{}.{}.{}.{}.{}.{}.{}.{}' +_TEMPLATE = 'cmip5.{}.{}.{}.{}.{}.{}.{}.{}.{}' # Collections injected into template. _COLLECTIONS = ( @@ -33,7 +33,8 @@ 'wcrp:cmip5:time-frequency', 'wcrp:cmip5:realm', 'wcrp:cmip5:cmor-table', - 'wcrp:cmip5:ensemble' + 'wcrp:cmip5:ensemble', + 'wcrp:cmip5:version' ) # Instantiated & cached parser instance. @@ -49,8 +50,7 @@ def parse(identifier): if _PARSER is None: _PARSER = create_template_parser(_TEMPLATE, _COLLECTIONS, PARSING_STRICTNESS_1) - # Strip version suffix. - if '#' in identifier: - identifier = identifier.split('#')[0] + # Convert version suffix to an identifier element. + identifier = identifier.replace('#', '.v') - return _PARSER.parse(identifier.split('#')[0]) + return _PARSER.parse(identifier) diff --git a/pyessv/_parsers/cmip6_dataset_id.py b/pyessv/_parsers/cmip6_dataset_id.py index e7cd2c0..34f39c8 100644 --- a/pyessv/_parsers/cmip6_dataset_id.py +++ b/pyessv/_parsers/cmip6_dataset_id.py @@ -19,7 +19,7 @@ _INI_PATTERN = 'CMIP6.%(activity_id)s.%(institution_id)s.%(source_id)s.%(experiment_id)s.%(member_id)s.%(table_id)s.%(variable_id)s.%(grid_label)s' # Template that identifiers must conform to. -_TEMPLATE = 'CMIP6.{}.{}.{}.{}.{}.{}.{}.{}' +_TEMPLATE = 'CMIP6.{}.{}.{}.{}.{}.{}.{}.{}.{}' # Collections injected into template. _COLLECTIONS = ( @@ -30,7 +30,8 @@ 'wcrp:cmip6:member-id', 'wcrp:cmip6:table-id', 'wcrp:cmip6:variable-id', - 'wcrp:cmip6:grid-label' + 'wcrp:cmip6:grid-label', + 'wcrp:cmip6:version' ) # Instantiated & cached parser instance. @@ -46,8 +47,7 @@ def parse(identifier): if _PARSER is None: _PARSER = create_template_parser(_TEMPLATE, _COLLECTIONS, PARSING_STRICTNESS_1) - # Strip version suffix. - if '#' in identifier: - identifier = identifier.split('#')[0] + # Convert version suffix to an identifier element. + identifier = identifier.replace('#', '.v') return _PARSER.parse(identifier) diff --git a/pyessv/_parsers/cordex_dataset_id.py b/pyessv/_parsers/cordex_dataset_id.py index 5cf01bb..962deaf 100644 --- a/pyessv/_parsers/cordex_dataset_id.py +++ b/pyessv/_parsers/cordex_dataset_id.py @@ -22,7 +22,7 @@ _TEST_IDENTIFIER = 'cordex.output.AFR-44.MOHC.MOHC-HadGEM2-ES.rcp60.r12i1p1.hadgem3-ra.v1.mon.areacella' # Template that identifiers must conform to. -_TEMPLATE = 'cordex.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}' +_TEMPLATE = 'cordex.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}' # Collections injected into template. _COLLECTIONS = ( @@ -36,6 +36,7 @@ 'wcrp:cordex:rcm-version', 'wcrp:cordex:time-frequency', 'wcrp:cordex:variable', + 'wcrp:cordex:version' ) # Instantiated & cached parser instance. @@ -50,8 +51,7 @@ def parse(identifier): if _PARSER is None: _PARSER = create_template_parser(_TEMPLATE, _COLLECTIONS, PARSING_STRICTNESS_1) - # Strip version suffix. - if '#' in identifier: - identifier = identifier.split('#')[0] + # Convert version suffix to an identifier element. + identifier = identifier.replace('#', '.v') return _PARSER.parse(identifier) From e3395d8c0790224665576f516d9dbacf0ac245da Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 3 Apr 2019 10:53:52 +0200 Subject: [PATCH 03/14] Create and return also virtual terms from template parsing --- pyessv/_parser_template.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyessv/_parser_template.py b/pyessv/_parser_template.py index 69a46ed..d056a5f 100644 --- a/pyessv/_parser_template.py +++ b/pyessv/_parser_template.py @@ -15,7 +15,7 @@ from pyessv._model.collection import Collection from pyessv._model.term import Term from pyessv._utils.compat import basestring - +import pyessv class TemplateParser(object): @@ -66,11 +66,13 @@ def parse(self, val): raise TemplateParsingError('{} :: {}'.format(name, val)) continue - # Verify collection match. + # Verify collection match & create a virtual pyessv.Term. collection = template_part term = collection.is_matched(name, self.strictness) if term == False: raise TemplateParsingError('vocab={} :: strictness={} :: value={}'.format(collection, self.strictness, val)) + term = pyessv.create_term(collection, name) + if isinstance(term, Term): terms.add(term) From 37cc549b7947d9fe83fd6efd33e7405e3cee9232 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Thu, 4 Apr 2019 13:27:25 +0200 Subject: [PATCH 04/14] Automate template parsing + Add template info to pyessv.archive --- pyessv/__init__.py | 5 ++ pyessv/_exceptions.py | 12 +++ pyessv/_factory.py | 8 +- pyessv/_loader.py | 10 +++ pyessv/_model/collection.py | 3 +- pyessv/_parser_template.py | 20 +++-- pyessv/_parsers/__init__.py | 60 ++------------ pyessv/_parsers/cmip5_dataset_id.py | 56 ------------- pyessv/_parsers/cordex_dataset_id.py | 2 +- pyessv/_parsers/dataset_id.py | 93 +++++++++++++++++++++ pyessv/_parsers/directory.py | 91 +++++++++++++++++++++ pyessv/_parsers/filename.py | 100 +++++++++++++++++++++++ pyessv/_parsers/input4mips_dataset_id.py | 2 +- sh/writers/esgf/map.py | 9 +- sh/writers/esgf/map_c3s_cmip5.py | 41 +++++++++- sh/writers/esgf/map_c3s_cordex.py | 46 ++++++++++- sh/writers/esgf/map_cc4e.py | 40 ++++++++- sh/writers/esgf/map_cmip5.py | 40 ++++++++- sh/writers/esgf/map_cmip6.py | 37 ++++++++- sh/writers/esgf/map_cordex.py | 46 ++++++++++- sh/writers/esgf/map_cordex_adjust.py | 46 ++++++++++- sh/writers/esgf/map_e3sm.py | 37 --------- sh/writers/esgf/map_euclipse.py | 40 ++++++++- sh/writers/esgf/map_geomip.py | 40 ++++++++- sh/writers/esgf/map_input4mips.py | 41 +++++++++- sh/writers/esgf/map_isimip_ft.py | 50 +++++++++++- sh/writers/esgf/map_lucid.py | 40 ++++++++- sh/writers/esgf/map_obs4mips.py | 38 +++++++-- sh/writers/esgf/map_pmip3.py | 40 ++++++++- sh/writers/esgf/map_primavera.py | 40 ++++++++- sh/writers/esgf/map_tamip.py | 40 ++++++++- sh/writers/wcrp/cmip6/write.py | 15 +++- 32 files changed, 953 insertions(+), 235 deletions(-) delete mode 100644 pyessv/_parsers/cmip5_dataset_id.py create mode 100644 pyessv/_parsers/dataset_id.py create mode 100644 pyessv/_parsers/directory.py create mode 100644 pyessv/_parsers/filename.py delete mode 100644 sh/writers/esgf/map_e3sm.py diff --git a/pyessv/__init__.py b/pyessv/__init__.py index 230d71c..c128667 100644 --- a/pyessv/__init__.py +++ b/pyessv/__init__.py @@ -65,6 +65,7 @@ from pyessv._loader import load_random from pyessv._loader import load +from pyessv._loader import all_scopes from pyessv._model import Authority from pyessv._model import Collection @@ -74,6 +75,10 @@ from pyessv._parser import parse from pyessv._parsers import parse_dataset_identifier from pyessv._parsers import parse_dataset_identifiers +from pyessv._parsers import parse_directory +from pyessv._parsers import parse_directories +from pyessv._parsers import parse_filename +from pyessv._parsers import parse_filenames from pyessv._utils.logger import log from pyessv._utils.logger import log_error diff --git a/pyessv/_exceptions.py b/pyessv/_exceptions.py index 25865ce..7a5e8ef 100644 --- a/pyessv/_exceptions.py +++ b/pyessv/_exceptions.py @@ -35,6 +35,18 @@ def __init__(self, val): super(TemplateParsingError, self).__init__(msg) +class TemplateValueError(ValueError): + """A template parsing error raised. + + """ + def __init__(self, val): + """Object constructor. + + """ + msg = 'A template value error has occurred: {}'.format(val) + super(TemplateValueError, self).__init__(msg) + + class ValidationError(ValueError): """A validation error raised by the package validator. diff --git a/pyessv/_factory.py b/pyessv/_factory.py index 7afbf41..69f835e 100644 --- a/pyessv/_factory.py +++ b/pyessv/_factory.py @@ -197,13 +197,13 @@ def _callback(instance): ) -def create_template_parser(template, collections, strictness=PARSING_STRICTNESS_2, seperator='.'): +def create_template_parser(template, collections, strictness=PARSING_STRICTNESS_2, separator='.'): """Instantiates, initialises & returns a template parser. :param str template: An expression template. :param tuple collections: Collections that the template maps to. :param int strictness: Strictness level to apply when applying name matching rules. - :param str seprarator: Seperator to apply when parsing. + :param str separator: Separator to apply when parsing. :returns: A vocabulary expression parser. :rtype: pyessv.TemplateParser @@ -216,9 +216,9 @@ def create_template_parser(template, collections, strictness=PARSING_STRICTNESS_ assert len(collections) > 0, 'Invalid collections' assert template.count('{}') == len(collections), 'Invalid template: collection count mismatch' assert strictness in PARSING_STRICTNESS_SET, 'Invalid parsing strictness: {}'.format(strictness) - assert isinstance(seperator, basestring), 'Invalid seperator' + assert isinstance(separator, basestring), 'Invalid separator' - return TemplateParser(template, collections, strictness, seperator) + return TemplateParser(template, collections, strictness, separator) def _create_node( diff --git a/pyessv/_loader.py b/pyessv/_loader.py index e3b622c..4be953e 100644 --- a/pyessv/_loader.py +++ b/pyessv/_loader.py @@ -26,6 +26,16 @@ from pyessv._utils.formatter import format_string +def all_scopes(): + """Returns all scopes. + + """ + scopes = set() + for authority in load(): + for scope in authority.scopes: + scopes.add(scope) + return scopes + def load(identifier=None, verbose=True): """Loads a vocabulary node from archive. diff --git a/pyessv/_model/collection.py b/pyessv/_model/collection.py index 4d52a80..997fed6 100644 --- a/pyessv/_model/collection.py +++ b/pyessv/_model/collection.py @@ -65,7 +65,8 @@ def is_virtual(self): """Gets flag indicating whether the collection is a virtual one (i.e. simply constrained by a reg-ex). """ - return len(self) == 0 + return self.term_regex is not None + #return len(self) == 0 def get_validators(self): diff --git a/pyessv/_parser_template.py b/pyessv/_parser_template.py index d056a5f..95b136e 100644 --- a/pyessv/_parser_template.py +++ b/pyessv/_parser_template.py @@ -11,7 +11,7 @@ """ -from pyessv._exceptions import TemplateParsingError +from pyessv._exceptions import TemplateParsingError, TemplateValueError from pyessv._model.collection import Collection from pyessv._model.term import Term from pyessv._utils.compat import basestring @@ -22,19 +22,19 @@ class TemplateParser(object): """A vocabulary template parser. """ - def __init__(self, template, collections, strictness, seperator='.'): + def __init__(self, template, collections, strictness, separator='.'): """Instance constructor. :param str template: Identifier template. :param list collections: pyessv collection identifiers. :param int strictness: Strictness level to apply when applying name matching rules. - :param str seprarator: Seperator to apply when parsing. + :param str seprarator: Separator to apply when parsing. """ from pyessv._loader import load - self.seperator = seperator - self.template_parts = template.split(seperator) + self.separator = separator + self.template_parts = template.split(separator) self.template = template self.strictness = strictness @@ -53,7 +53,7 @@ def parse(self, val): """ # Verify that number of parts is equal. - parts = val.split(self.seperator) + parts = val.split(self.separator) if len(parts) != len(self.template_parts): raise TemplateParsingError('Number of elements is invalid: {}: is {}, expected {}'.format(val, len(parts), len(self.template_parts))) @@ -66,14 +66,16 @@ def parse(self, val): raise TemplateParsingError('{} :: {}'.format(name, val)) continue - # Verify collection match & create a virtual pyessv.Term. + # Verify collection match. collection = template_part term = collection.is_matched(name, self.strictness) if term == False: - raise TemplateParsingError('vocab={} :: strictness={} :: value={}'.format(collection, self.strictness, val)) - term = pyessv.create_term(collection, name) + raise TemplateValueError('vocab={} :: strictness={} :: value={}'.format(collection, self.strictness, name)) + # Create a virtual term if needed. if isinstance(term, Term): terms.add(term) + else: + terms.add(pyessv.create_term(collection, name)) return terms diff --git a/pyessv/_parsers/__init__.py b/pyessv/_parsers/__init__.py index a836502..5402385 100644 --- a/pyessv/_parsers/__init__.py +++ b/pyessv/_parsers/__init__.py @@ -11,58 +11,10 @@ """ -import collections -from pyessv._parsers.cmip5_dataset_id import parse as parse_cmip5_dataset_id -from pyessv._parsers.cmip6_dataset_id import parse as parse_cmip6_dataset_id -from pyessv._parsers.cordex_dataset_id import parse as parse_cordex_dataset_id -from pyessv._parsers.input4mips_dataset_id import parse as parse_input4mips_dataset_id -from pyessv._utils.compat import basestring - - - -# Map of dataset id parsers to projects. -_DATASET_ID_PARSERS = { - 'cmip5': parse_cmip5_dataset_id, - 'cmip6': parse_cmip6_dataset_id, - 'cordex': parse_cordex_dataset_id, - 'input4mips': parse_input4mips_dataset_id -} - - -def parse_dataset_identifiers(project, identifiers): - """Parses a collection of dataset identifiers. - - :param str project: Project code. - :param iterable identifiers: Dataset identifiers. - - :returns: Facets extracted from the identifiers. - :rtype: list - - """ - assert isinstance(identifiers, collections.Iterable), 'Invalid identifiers' - - result = set() - for identifier in identifiers: - result = result.union(parse_dataset_identifier(project, identifier)) - - return result - - -def parse_dataset_identifier(project, identifier): - """Parses a dataset identifier. - - :param str project: Project code. - :param str identifier: Dataset identifier. - - :returns: Set of terms extracted from the identifier. - :rtype: set - - """ - assert isinstance(project, basestring), 'Invalid project' - assert project in _DATASET_ID_PARSERS, 'Unsupported project' - assert isinstance(identifier, basestring), 'Invalid identifier' - - parser = _DATASET_ID_PARSERS[project] - - return parser(identifier) +from pyessv._parsers.dataset_id import parse_dataset_identifier +from pyessv._parsers.dataset_id import parse_dataset_identifiers +from pyessv._parsers.directory import parse_directories +from pyessv._parsers.directory import parse_directory +from pyessv._parsers.filename import parse_filename +from pyessv._parsers.filename import parse_filenames diff --git a/pyessv/_parsers/cmip5_dataset_id.py b/pyessv/_parsers/cmip5_dataset_id.py deleted file mode 100644 index 36c3fa0..0000000 --- a/pyessv/_parsers/cmip5_dataset_id.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -.. module:: pyessv._parsers.cmip6_dataset_id.py - :copyright: Copyright "December 01, 2016", IPSL - :license: GPL/CeCIL - :platform: Unix, Windows - :synopsis: Encapsulates parsing of a CMIP5 dataset identifier. - -.. moduleauthor:: Mark Conway-Greenslade - -""" -from pyessv._factory import create_template_parser -from pyessv._constants import PARSING_STRICTNESS_1 - - - -# Template extracted from esgf ini file (for reference purpose only). -_INI_PATTERN = 'cmip5.%(product)s.%(institute)s.%(model)s.%(experiment)s.%(time_frequency)s.%(realm)s.%(cmor_table)s.%(ensemble)s' - -# Test identifier (for reference purpose only). -_TEST_IDENTIFIER = 'cmip5.output2.IPSL.IPSL-CM5A-LR.historicalMisc.mon.ocean.Omon.r2i1p1.v20150504' - -# Template that identifiers must conform to. -_TEMPLATE = 'cmip5.{}.{}.{}.{}.{}.{}.{}.{}.{}' - -# Collections injected into template. -_COLLECTIONS = ( - 'wcrp:cmip5:product', - 'wcrp:cmip5:institute', - 'wcrp:cmip5:model', - 'wcrp:cmip5:experiment', - 'wcrp:cmip5:time-frequency', - 'wcrp:cmip5:realm', - 'wcrp:cmip5:cmor-table', - 'wcrp:cmip5:ensemble', - 'wcrp:cmip5:version' - ) - -# Instantiated & cached parser instance. -_PARSER = None - - -def parse(identifier): - """Parses a CMIP6 dataset identifier. - - """ - # Instantiate parser JIT. - global _PARSER - if _PARSER is None: - _PARSER = create_template_parser(_TEMPLATE, _COLLECTIONS, PARSING_STRICTNESS_1) - - # Convert version suffix to an identifier element. - identifier = identifier.replace('#', '.v') - - return _PARSER.parse(identifier) diff --git a/pyessv/_parsers/cordex_dataset_id.py b/pyessv/_parsers/cordex_dataset_id.py index 962deaf..28d280b 100644 --- a/pyessv/_parsers/cordex_dataset_id.py +++ b/pyessv/_parsers/cordex_dataset_id.py @@ -43,7 +43,7 @@ _PARSER = None def parse(identifier): - """Parses a CMIP6 dataset identifier. + """Parses a CORDEX dataset identifier. """ # Instantiate parser JIT. diff --git a/pyessv/_parsers/dataset_id.py b/pyessv/_parsers/dataset_id.py new file mode 100644 index 0000000..e4b2234 --- /dev/null +++ b/pyessv/_parsers/dataset_id.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +""" +.. module:: pyessv._parsers.dataset_id.py + :copyright: Copyright "December 01, 2016", IPSL + :license: GPL/CeCIL + :platform: Unix, Windows + :synopsis: Encapsulates parsing of an ESGF dataset identifier. + +.. moduleauthor:: Mark Conway-Greenslade + +""" +import collections + +from pyessv import all_scopes +from pyessv._constants import PARSING_STRICTNESS_1 +from pyessv._factory import create_template_parser +from pyessv._utils.compat import basestring + +# Template extracted from esgf ini file (for reference purpose only). +_INI_PATTERN = 'cmip5.%(product)s.%(institute)s.%(model)s.%(experiment)s.%(time_frequency)s.%(realm)s.%(cmor_table)s.%(ensemble)s' + +# Test identifier (for reference purpose only). +_TEST_IDENTIFIER = 'cmip5.output2.IPSL.IPSL-CM5A-LR.historicalMisc.mon.ocean.Omon.r2i1p1.v20150504' + +# Instantiated & cached parser instance. +_PARSER = None + + +def parse_dataset_identifiers(project, identifiers): + """Parses a collection of dataset identifiers. + + :param str project: Project code. + :param iterable identifiers: Dataset identifiers. + + :returns: Facets extracted from the identifiers. + :rtype: list + + """ + assert isinstance(identifiers, collections.Iterable), 'Invalid identifiers' + + result = set() + for identifier in identifiers: + result = result.union(parse_dataset_identifier(project, identifier)) + + return result + + +def parse_dataset_identifier(project, identifier): + """Parses a dataset identifier. + + :param str project: Project code. + :param str identifier: Dataset identifier. + + :returns: Set of terms extracted from the identifier. + :rtype: set + + """ + assert isinstance(project, basestring), 'Invalid project' + assert isinstance(identifier, basestring), 'Invalid identifier' + + # Instantiated template + _TEMPLATE = None + + # Instantiated template collections + _COLLECTIONS = None + + # Get scope corresponding to the project code. + scopes = all_scopes() + assert project in [scope.name for scope in scopes], 'Unsupported project' + scope = [scope for scope in scopes if scope.name == project][0] + + # Get template from data scope. + assert 'dataset_id_template' in scope.data.keys(), 'Dataset ID template not found' + _TEMPLATE = scope.data['dataset_id_template'] + assert isinstance(_TEMPLATE, basestring), 'Invalid template' + + # Get template collections from data scope. + assert 'dataset_id_collections' in scope.data.keys(), 'Template collections not found' + _COLLECTIONS = list() + for name in scope.data['dataset_id_collections']: + _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) + assert _COLLECTIONS, 'Invalid collections' + + # Instantiate parser JIT. + global _PARSER + if _PARSER is None: + _PARSER = create_template_parser(_TEMPLATE, tuple(_COLLECTIONS), PARSING_STRICTNESS_1) + + # Convert version suffix to an identifier element. + identifier = identifier.replace('#', '.v') + + return _PARSER.parse(identifier) diff --git a/pyessv/_parsers/directory.py b/pyessv/_parsers/directory.py new file mode 100644 index 0000000..f85eebf --- /dev/null +++ b/pyessv/_parsers/directory.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +""" +.. module:: pyessv._parsers.directory.py + :copyright: Copyright "December 01, 2016", IPSL + :license: GPL/CeCIL + :platform: Unix, Windows + :synopsis: Encapsulates parsing of an ESGF directory. + +.. moduleauthor:: Mark Conway-Greenslade + +""" +import collections + +from pyessv import all_scopes +from pyessv._constants import PARSING_STRICTNESS_1 +from pyessv._factory import create_template_parser +from pyessv._utils.compat import basestring + +# Template extracted from esgf ini file (for reference purpose only). +_INI_PATTERN = '%(root)s/%(project)s/%(product)s/%(institute)s/%(model)s/%(experiment)s/%(time_frequency)s/%(realm)s/%(cmor_table)s/%(ensemble)s/%(version)s/%(variable)s' + +# Test directory (for reference purpose only). +_TEST_DIRECTORY = 'CMIP5/output1/IPSL/IPSL-CM5A-LR/1pctCO2/mon/atmos/Amon/r1i1p1/v20110427/tas' +_TEST_DIRECTORY = 'CMIP5/output1/IPSL/IPSL-CM5A-LR/1pctCO2/mon/atmos/Amon/r1i1p1/latest/tas' + +# Instantiated & cached parser instance. +_PARSER = None + + +def parse_directories(project, directories): + """Parses a collection of directories. + + :param str project: Project code. + :param iterable directories: Data directories. + + :returns: Facets extracted from the directories. + :rtype: list + + """ + assert isinstance(directories, collections.Iterable), 'Invalid directories' + + result = set() + for directory in directories: + result = result.union(parse_directory(project, directory)) + + return result + + +def parse_directory(project, directory): + """Parses a directory. + + :param str project: Project code. + :param str directory: Data directory. + + :returns: Set of terms extracted from the directory. + :rtype: set + + """ + assert isinstance(project, basestring), 'Invalid project' + assert isinstance(directory, basestring), 'Invalid directory' + + # Instantiated template + _TEMPLATE = None + + # Instantiated template collections + _COLLECTIONS = None + + # Get scope corresponding to the project code. + scopes = all_scopes() + assert project in [scope.name for scope in scopes], 'Unsupported project' + scope = [scope for scope in scopes if scope.name == project][0] + + # Get template from data scope. + assert 'directory_template' in scope.data.keys(), 'Directory template not found' + _TEMPLATE = scope.data['directory_template'] + assert isinstance(_TEMPLATE, basestring), 'Invalid template' + + # Get template collections from data scope. + assert 'directory_collections' in scope.data.keys(), 'Template collections not found' + _COLLECTIONS = list() + for name in scope.data['directory_collections']: + _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) + assert _COLLECTIONS, 'Invalid collections' + + # Instantiate parser JIT. + global _PARSER + if _PARSER is None: + _PARSER = create_template_parser(_TEMPLATE, tuple(_COLLECTIONS), PARSING_STRICTNESS_1, separator='/') + + return _PARSER.parse(directory) diff --git a/pyessv/_parsers/filename.py b/pyessv/_parsers/filename.py new file mode 100644 index 0000000..eeb34bd --- /dev/null +++ b/pyessv/_parsers/filename.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +""" +.. module:: pyessv._parsers.filename.py + :copyright: Copyright "December 01, 2016", IPSL + :license: GPL/CeCIL + :platform: Unix, Windows + :synopsis: Encapsulates parsing of an ESGF filename. + +.. moduleauthor:: Mark Conway-Greenslade + +""" +import collections +from os.path import splitext + +from pyessv import all_scopes +from pyessv._constants import PARSING_STRICTNESS_1 +from pyessv._exceptions import TemplateParsingError +from pyessv._factory import create_template_parser + +# Template extracted from esgf ini file (for reference purpose only). +_INI_PATTERN = '%(variable)s_%(cmor_table)s_%(model)s_%(experiment)s_%(ensemble)s[_%(period_start)s-%(period_end)s].nc' + +# Test filename (for reference purpose only). +_TEST_FILENAME = 'tas_Amon_IPSL-CM5A-LR_1pctCO2_r1i1p1_185001-198912.nc' +_TEST_FILENAME = 'orog_fx_IPSL-CM5A-LR_1pctCO2_r0i0p0.nc' +_TEST_FILENAME = 'tas_Amon_IPSL-CM5A-LR_1pctCO2_r1i1p1_185001-198912-clim.nc' + +# Instantiated & cached parser instance. +_PARSER = None + + +def parse_filenames(project, filenames): + """Parses a collection of filenames. + + :param str project: Project code. + :param iterable filenames: Filenames. + + :returns: Facets extracted from the filenames. + :rtype: list + + """ + assert isinstance(filenames, collections.Iterable), 'Invalid filenames' + + result = set() + for filename in filenames: + result = result.union(parse_filename(project, filename)) + + return result + + +def parse_filename(project, filename): + """Parses a filename. + + :param str project: Project code. + :param str filename: Filename. + + :returns: Set of terms extracted from the filename. + :rtype: set + + """ + assert isinstance(project, basestring), 'Invalid project' + assert isinstance(filename, basestring), 'Invalid filename' + + # Instantiated template + _TEMPLATE = None + + # Instantiated template collections + _COLLECTIONS = None + + # Get scope corresponding to the project code. + scopes = all_scopes() + assert project in [scope.name for scope in scopes], 'Unsupported project' + scope = [scope for scope in scopes if scope.name == project][0] + + # Get template from data scope. + assert 'filename_template' in scope.data.keys(), 'Filename template not found' + _TEMPLATE = scope.data['filename_template'] + assert isinstance(_TEMPLATE, basestring), 'Invalid template' + + # Get template collections from data scope. + assert 'filename_collections' in scope.data.keys(), 'Filename collections not found' + _COLLECTIONS = list() + for name in scope.data['filename_collections']: + _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) + assert _COLLECTIONS, 'Invalid collections' + + # Instantiate parser JIT. + global _PARSER + if _PARSER is None: + _PARSER = create_template_parser(_TEMPLATE, tuple(_COLLECTIONS), PARSING_STRICTNESS_1, separator='_') + + # Strip file extension. + filename = splitext(filename)[0] + + try: + return _PARSER.parse(filename) + except TemplateParsingError: + # Add suffix to filename without file period. + return _PARSER.parse(filename + '_fixed') diff --git a/pyessv/_parsers/input4mips_dataset_id.py b/pyessv/_parsers/input4mips_dataset_id.py index 92f9a03..f01a245 100644 --- a/pyessv/_parsers/input4mips_dataset_id.py +++ b/pyessv/_parsers/input4mips_dataset_id.py @@ -37,7 +37,7 @@ def parse(identifier): - """Parses a CMIP6 dataset identifier. + """Parses a input4MIPs dataset identifier. """ print identifier diff --git a/sh/writers/esgf/map.py b/sh/writers/esgf/map.py index 8dc67db..019e5ad 100644 --- a/sh/writers/esgf/map.py +++ b/sh/writers/esgf/map.py @@ -24,7 +24,6 @@ import map_cmip6 import map_cordex import map_cordex_adjust -import map_e3sm import map_euclipse import map_geomip import map_input4mips @@ -57,7 +56,6 @@ map_cmip6, map_cordex, map_cordex_adjust, - map_e3sm, map_euclipse, map_geomip, map_input4mips, @@ -105,9 +103,10 @@ def _main(args): scope = _create_scope(authority, project) # Set scope data. - scope.data = scope.data or dict() - for field in module.SCOPE_DATA: - scope.data[field] = ini_section.get_option(field, raw=True) + scope.data = module.SCOPE_DATA or dict() + #scope.data = scope.data or dict() + #for field in module.SCOPE_DATA: + # scope.data[field] = ini_section.get_option(field, raw=True) # Create regex collections. collections = [i for i in module.COLLECTIONS if not inspect.isfunction(i[1])] diff --git a/sh/writers/esgf/map_c3s_cmip5.py b/sh/writers/esgf/map_c3s_cmip5.py index e96d2dd..8331474 100644 --- a/sh/writers/esgf/map_c3s_cmip5.py +++ b/sh/writers/esgf/map_c3s_cmip5.py @@ -28,7 +28,8 @@ ('realm', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), - ('version', r'^v[0-9]*$') + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Arbitrary data associated with a collection. @@ -45,9 +46,41 @@ # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ), + 'directory_template': 'C3S-CMIP5/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'variable', + 'dataset_version' + ), + 'dataset_id_template': 'c3s-cmip5.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'variable', + 'dataset_version' + ) } diff --git a/sh/writers/esgf/map_c3s_cordex.py b/sh/writers/esgf/map_c3s_cordex.py index cb4d119..58fea71 100644 --- a/sh/writers/esgf/map_c3s_cordex.py +++ b/sh/writers/esgf/map_c3s_cordex.py @@ -29,14 +29,52 @@ ('thredds_exclude_variables', yield_comma_delimited_options), ('time_frequency', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), - ('version', r'v^[0-9]*$') + ('dataset_version', r'latest|v^[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable', + 'domain', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_model', + 'rcm_version', + 'time_frequency', + 'file_period' + ), + 'directory_template': 'C3S-CORDEX/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'product', + 'domain', + 'institute', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_model', + 'rcm_version', + 'time_frequency', + 'variable', + 'dataset_version' + ), + 'dataset_id_template': 'c3s-cordex.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'product', + 'domain', + 'institute', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_name', + 'rcm_version', + 'time_frequency', + 'variable', + 'version' + ) } diff --git a/sh/writers/esgf/map_cc4e.py b/sh/writers/esgf/map_cc4e.py index d6c75a2..2390d85 100644 --- a/sh/writers/esgf/map_cc4e.py +++ b/sh/writers/esgf/map_cc4e.py @@ -23,12 +23,44 @@ ('time_frequency', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), - ('version', r'^v[0-9]*$') + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_cc4e_{}_{}_{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable', + 'work_package', + 'product', + 'source_type', + 'source_data_id', + 'realization', + 'domain', + 'time_frequency', + 'file_period' + ), + 'directory_template': 'CC4E/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'work_package', + 'product', + 'source_type', + 'source_data_id', + 'realization', + 'time_frequency', + 'variable', + 'dataset_version' + ), + 'dataset_id_template': 'CC4E.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'work_package', + 'product', + 'source_type', + 'source_data_id', + 'realization', + 'time_frequency', + 'variable', + 'dataset_version' + ) } diff --git a/sh/writers/esgf/map_cmip5.py b/sh/writers/esgf/map_cmip5.py index fcc374c..ca17f41 100644 --- a/sh/writers/esgf/map_cmip5.py +++ b/sh/writers/esgf/map_cmip5.py @@ -29,7 +29,8 @@ ('realm', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), - ('version', r'^v[0-9]*$') + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Arbitrary data associated with a collection. @@ -46,9 +47,40 @@ # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ), + 'directory_template': 'CMIP5/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version', + 'variable' + ), + 'dataset_id_template': 'cmip5.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version' + ) } diff --git a/sh/writers/esgf/map_cmip6.py b/sh/writers/esgf/map_cmip6.py index ccf092a..31f749c 100644 --- a/sh/writers/esgf/map_cmip6.py +++ b/sh/writers/esgf/map_cmip6.py @@ -23,9 +23,40 @@ # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable_id', + 'table_id', + 'source_id', + 'experiment_id', + 'member_id', + 'grid_label' + 'file_period' + ), + 'directory_template': 'CMIP6/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'activity_id', + 'institution_id', + 'source_id', + 'experiment_id', + 'member_id', + 'table_id', + 'variable_id', + 'grid_label', + 'dataset_version' + ), + 'dataset_id_template': 'CMIP6.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'activity_id', + 'institution_id', + 'source_id', + 'experiment_id', + 'member_id', + 'table_id', + 'variable_id', + 'grid_label', + 'dataset_version' + ) } diff --git a/sh/writers/esgf/map_cordex.py b/sh/writers/esgf/map_cordex.py index 85f6c75..5ee5abe 100644 --- a/sh/writers/esgf/map_cordex.py +++ b/sh/writers/esgf/map_cordex.py @@ -31,14 +31,52 @@ ('thredds_exclude_variables', yield_comma_delimited_options), ('time_frequency', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), - ('version', r'v^[0-9]*$') + ('dataset_version', r'latest|v^[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable', + 'domain', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_model', + 'rcm_version', + 'time_frequency', + 'file_period' + ), + 'directory_template': 'CORDEX/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'product', + 'domain', + 'institute', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_model', + 'rcm_version', + 'time_frequency', + 'variable', + 'dataset_version' + ), + 'dataset_id_template': 'cordex.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'product', + 'domain', + 'institute', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_name', + 'rcm_version', + 'time_frequency', + 'variable', + 'version' + ) } diff --git a/sh/writers/esgf/map_cordex_adjust.py b/sh/writers/esgf/map_cordex_adjust.py index 37e0350..afe1810 100644 --- a/sh/writers/esgf/map_cordex_adjust.py +++ b/sh/writers/esgf/map_cordex_adjust.py @@ -31,14 +31,52 @@ ('thredds_exclude_variables', yield_comma_delimited_options), ('time_frequency', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), - ('version', r'v^[0-9]*$') + ('dataset_version', r'latest|v^[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable', + 'domain', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_model', + 'bias_adjustment', + 'time_frequency', + 'file_period' + ), + 'directory_template': 'CORDEX-Adjust/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'product', + 'domain', + 'institute', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_model', + 'bias_adjustment', + 'time_frequency', + 'variable', + 'dataset_version' + ), + 'dataset_id_template': 'cordex-adjust.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'product', + 'domain', + 'institute', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_name', + 'bias_adjustment', + 'time_frequency', + 'variable', + 'version' + ) } diff --git a/sh/writers/esgf/map_e3sm.py b/sh/writers/esgf/map_e3sm.py deleted file mode 100644 index 1cfd0c5..0000000 --- a/sh/writers/esgf/map_e3sm.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -.. module:: map_e3sm.py - :license: GPL/CeCIL - :platform: Unix, Windows - :synopsis: Maps E3SM ESGF publisher ini file to normalized pyessv format. - -.. moduleauthor:: Mark Conway-Greenslade - -""" -from utils import yield_comma_delimited_options -from utils import yield_pipe_delimited_options - - -# Vocabulary collections extracted from ini file. -COLLECTIONS = { - ('source', yield_comma_delimited_options), - ('model_version', yield_comma_delimited_options), - ('experiment', yield_pipe_delimited_options), - ('atmos_grid_resolution', yield_comma_delimited_options), - ('ocean_grid_resolution', yield_comma_delimited_options), - ('realm', yield_comma_delimited_options), - ('regridding', yield_comma_delimited_options), - ('data_type', yield_comma_delimited_options), - ('time_frequency', yield_comma_delimited_options), - ('ensemble_member', r'^[A-Za-z0-9]*$'), - ('thredds_exclude_variables', yield_comma_delimited_options), - ('version', r'^v[0-9]*$') -} - -# Fields extracted from ini file & appended as data to the scope. -SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' -} diff --git a/sh/writers/esgf/map_euclipse.py b/sh/writers/esgf/map_euclipse.py index 98af265..edf2f47 100644 --- a/sh/writers/esgf/map_euclipse.py +++ b/sh/writers/esgf/map_euclipse.py @@ -29,15 +29,47 @@ ('realm', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), - ('version', r'^v[0-9]*$') + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ), + 'directory_template': 'EUCLIPSE/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version', + 'variable' + ), + 'dataset_id_template': 'euclipse.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version' + ) } diff --git a/sh/writers/esgf/map_geomip.py b/sh/writers/esgf/map_geomip.py index 485cdf3..920ec1d 100644 --- a/sh/writers/esgf/map_geomip.py +++ b/sh/writers/esgf/map_geomip.py @@ -29,14 +29,46 @@ ('realm', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), - ('version', r'^v[0-9]*$') + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ), + 'directory_template': 'GeoMIP/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version', + 'variable' + ), + 'dataset_id_template': 'geomip.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version' + ) } diff --git a/sh/writers/esgf/map_input4mips.py b/sh/writers/esgf/map_input4mips.py index 4d160b1..8272c83 100644 --- a/sh/writers/esgf/map_input4mips.py +++ b/sh/writers/esgf/map_input4mips.py @@ -23,16 +23,49 @@ ('grid_label', yield_comma_delimited_options), ('institution_id', yield_comma_delimited_options), ('realm', yield_comma_delimited_options), + ('mip_era', yield_comma_delimited_options), ('frequency', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), - ('version', r'^v[0-9]{8}$'), + ('dataset_version', r'latest|^v[0_9]{8}$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable_id', + 'activity_id', + 'dataset_category', + 'target_mip', + 'source_id', + 'grid_label' + 'file_period' + ), + 'directory_template': 'input4MIPs/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'mip_era', + 'target_mip', + 'institution_id', + 'source_id', + 'realm', + 'frequency', + 'variable_id', + 'grid_label', + 'dataset_version' + ), + 'dataset_id_template': 'input4MIPs.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'mip_era', + 'target_mip', + 'institution_id', + 'source_id', + 'realm', + 'frequency', + 'variable_id', + 'grid_label', + 'dataset_version' + ) } def yield_variable_id_options(ctx): diff --git a/sh/writers/esgf/map_isimip_ft.py b/sh/writers/esgf/map_isimip_ft.py index ab0e368..b60cf11 100644 --- a/sh/writers/esgf/map_isimip_ft.py +++ b/sh/writers/esgf/map_isimip_ft.py @@ -30,15 +30,57 @@ ('land_use_short', yield_comma_delimited_options), ('time_frequency', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), - ('version', r'^v[0-9]*$'), - ('thredds_exclude_variables', yield_comma_delimited_options) + ('dataset_version', r'latest|^v[0-9]*$'), + ('thredds_exclude_variables', yield_comma_delimited_options), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'impact_model', + 'model', + 'experiment', + 'social_forcing', + 'co2_forcing', + 'irrigation_forcing', + 'land_use_short', + 'variable' + 'time_frequency', + 'file_period' + ), + 'directory_template': 'ISIMIP-FT/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'product', + 'model', + 'experiment', + 'impact_model', + 'sector_short', + 'social_forcing', + 'co2_forcing', + 'irrigation_forcing', + 'land_use_short', + 'variable' + 'time_frequency', + 'dataset_version' + ), + 'dataset_id_template': 'isimip-ft.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'product', + 'impact_model', + 'sector_short', + 'model', + 'experiment', + 'social_forcing', + 'co2_forcing', + 'irrigation_forcing', + 'time_frequency', + 'land_use_short', + 'variable' + 'dataset_version' + ) } diff --git a/sh/writers/esgf/map_lucid.py b/sh/writers/esgf/map_lucid.py index 984957e..52cf45c 100644 --- a/sh/writers/esgf/map_lucid.py +++ b/sh/writers/esgf/map_lucid.py @@ -29,14 +29,46 @@ ('realm', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), - ('version', r'^v[0-9]*$') + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ), + 'directory_template': 'LUCID/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version', + 'variable' + ), + 'dataset_id_template': 'lucid.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version' + ) } diff --git a/sh/writers/esgf/map_obs4mips.py b/sh/writers/esgf/map_obs4mips.py index 260019e..e882fa3 100644 --- a/sh/writers/esgf/map_obs4mips.py +++ b/sh/writers/esgf/map_obs4mips.py @@ -6,7 +6,7 @@ :platform: Unix, Windows :synopsis: Maps obs4MIPs ESGF publisher ini file to normalized pyessv format. -.. moduleauthor:: Mark Conway-Greenslade +.. moduleauthor:: Mark Conway_Greenslade """ from utils import yield_comma_delimited_options @@ -24,15 +24,43 @@ ('time_frequency', yield_comma_delimited_options), ('data_structure', yield_comma_delimited_options), ('source_id', yield_comma_delimited_options), - ('version', r'^v[0-9]*$'), + ('dataset_version', r'latest|^v[0-9]*$'), + ('processing_level', r'^[A-Za-z0-9]*$'), + ('processing_version', r'^[A-Za-z0-9]*$'), ('las_time_delta', lambda: yield_las_time_delta), - ('thredds_exclude_variables', yield_comma_delimited_options) + ('thredds_exclude_variables', yield_comma_delimited_options), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable', + 'source_id', + 'processing_level', + 'processing_version', + 'file_period' + ), + 'directory_template': 'obs4MIPs/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'product', + 'realm', + 'var', + 'time_frequency', + 'data_structure', + 'institute', + 'source_id', + 'dataset_version' + ), + 'dataset_id_template': 'obs4MIPs.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'institute', + 'source_id', + 'variable', + 'time_frequency', + 'dataset_version' + ) } diff --git a/sh/writers/esgf/map_pmip3.py b/sh/writers/esgf/map_pmip3.py index 680fcf1..1664346 100644 --- a/sh/writers/esgf/map_pmip3.py +++ b/sh/writers/esgf/map_pmip3.py @@ -29,14 +29,46 @@ ('realm', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), - ('version', r'^v[0-9]*$') + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ), + 'directory_template': 'PMIP3/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version', + 'variable' + ), + 'dataset_id_template': 'pmip3.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version' + ) } diff --git a/sh/writers/esgf/map_primavera.py b/sh/writers/esgf/map_primavera.py index 65f4843..b1668c5 100644 --- a/sh/writers/esgf/map_primavera.py +++ b/sh/writers/esgf/map_primavera.py @@ -26,14 +26,46 @@ ('variable', r'^[A-Za-z0-9]*$'), ('grid_label', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), - ('version', r'^v[0-9]*$') + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'grid_label', + 'file_period' + ), + 'directory_template': 'PRIMAVERA/{}/{}=/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'activity', + 'institute', + 'model', + 'experiment', + 'ensemble', + 'cmor_table', + 'variable', + 'grid_version', + 'dataset_version' + ), + 'dataset_id_template': 'PRIMAVERA.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'activity', + 'institute', + 'model', + 'experiment', + 'ensemble', + 'cmor_table', + 'variable', + 'grid_version', + 'dataset_version' + ) } diff --git a/sh/writers/esgf/map_tamip.py b/sh/writers/esgf/map_tamip.py index a4e3dd1..e576438 100644 --- a/sh/writers/esgf/map_tamip.py +++ b/sh/writers/esgf/map_tamip.py @@ -29,14 +29,46 @@ ('realm', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), - ('version', r'^v[0-9]*$') + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_format', - 'directory_format', - 'dataset_id' + 'filename_template': '{}_{}_{}_{}_{}_{}', + 'filename_collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ), + 'directory_template': 'TAMIP/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'directory_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version', + 'variable' + ), + 'dataset_id_template': 'tamip.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'dataset_id_collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version' + ) } diff --git a/sh/writers/wcrp/cmip6/write.py b/sh/writers/wcrp/cmip6/write.py index dd349c5..20f4f6d 100644 --- a/sh/writers/wcrp/cmip6/write.py +++ b/sh/writers/wcrp/cmip6/write.py @@ -188,16 +188,25 @@ 'is_virtual': True, 'label': None, 'ommitted': [], - 'term_regex': r'^[A-Za-z0-9]*$', + 'term_regex': r'^[A-Za-z0-9]*$' }, - 'version': { + 'dataset_version': { 'cim_document_type': None, 'cim_document_type_alternative_name': None, 'data_factory': None, 'is_virtual': True, 'label': None, 'ommitted': [], - 'term_regex': r'^[0-9]{8}$', + 'term_regex': r'latest|^[0-9]{8}$' + }, + 'file_period': { + 'cim_document_type': None, + 'cim_document_type_alternative_name': None, + 'data_factory': None, + 'is_virtual': True, + 'label': None, + 'ommitted': [], + 'term_regex': r'fixed|^\d+-\d+(-clim)?$' } }, _SCOPE_GLOBAL: { From 50ed15f994f1df25e52d2fcff00f8e985b66bee3 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Thu, 4 Apr 2019 16:34:39 +0200 Subject: [PATCH 05/14] Parser caching depending on the project code --- pyessv/_parsers/dataset_id.py | 59 ++++++++++++++++++++--------------- pyessv/_parsers/directory.py | 59 ++++++++++++++++++++++------------- pyessv/_parsers/filename.py | 52 +++++++++++++++++------------- 3 files changed, 100 insertions(+), 70 deletions(-) diff --git a/pyessv/_parsers/dataset_id.py b/pyessv/_parsers/dataset_id.py index e4b2234..bda091d 100644 --- a/pyessv/_parsers/dataset_id.py +++ b/pyessv/_parsers/dataset_id.py @@ -17,13 +17,20 @@ from pyessv._factory import create_template_parser from pyessv._utils.compat import basestring -# Template extracted from esgf ini file (for reference purpose only). -_INI_PATTERN = 'cmip5.%(product)s.%(institute)s.%(model)s.%(experiment)s.%(time_frequency)s.%(realm)s.%(cmor_table)s.%(ensemble)s' - # Test identifier (for reference purpose only). -_TEST_IDENTIFIER = 'cmip5.output2.IPSL.IPSL-CM5A-LR.historicalMisc.mon.ocean.Omon.r2i1p1.v20150504' +_TEST_CMIP5_IDENTIFIER = 'cmip5.output2.IPSL.IPSL-CM5A-LR.historicalMisc.mon.ocean.Omon.r2i1p1.v20150504' +_TEST_CORDEX_IDENTIFIER = 'cordex.output.AFR-44.MOHC.MOHC-HadGEM2-ES.rcp60.r12i1p1.hadgem3-ra.v1.mon.areacella' + +# Instantiated template +_TEMPLATE = None + +# Instantiated template collections +_COLLECTIONS = None -# Instantiated & cached parser instance. +# Instantiated project. +_PROJECT = None + +# Instantiated parser. _PARSER = None @@ -59,34 +66,34 @@ def parse_dataset_identifier(project, identifier): assert isinstance(project, basestring), 'Invalid project' assert isinstance(identifier, basestring), 'Invalid identifier' - # Instantiated template - _TEMPLATE = None + global _PROJECT, _PARSER, _TEMPLATE, _COLLECTIONS - # Instantiated template collections - _COLLECTIONS = None + if _PROJECT != project: - # Get scope corresponding to the project code. - scopes = all_scopes() - assert project in [scope.name for scope in scopes], 'Unsupported project' - scope = [scope for scope in scopes if scope.name == project][0] + # Get scope corresponding to the project code. + scopes = all_scopes() + assert project in [scope.name for scope in scopes], 'Unsupported project' + scope = [scope for scope in scopes if scope.name == project][0] - # Get template from data scope. - assert 'dataset_id_template' in scope.data.keys(), 'Dataset ID template not found' - _TEMPLATE = scope.data['dataset_id_template'] - assert isinstance(_TEMPLATE, basestring), 'Invalid template' + # Get template from data scope. + assert 'dataset_id_template' in scope.data.keys(), 'Dataset ID template not found' + _TEMPLATE = scope.data['dataset_id_template'] + assert isinstance(_TEMPLATE, basestring), 'Invalid template' - # Get template collections from data scope. - assert 'dataset_id_collections' in scope.data.keys(), 'Template collections not found' - _COLLECTIONS = list() - for name in scope.data['dataset_id_collections']: - _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) - assert _COLLECTIONS, 'Invalid collections' + # Get template collections from data scope. + assert 'dataset_id_collections' in scope.data.keys(), 'Template collections not found' + _COLLECTIONS = list() + for name in scope.data['dataset_id_collections']: + _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) + assert _COLLECTIONS, 'Invalid collections' - # Instantiate parser JIT. - global _PARSER - if _PARSER is None: + # Instantiate parser JIT. + global _PARSER _PARSER = create_template_parser(_TEMPLATE, tuple(_COLLECTIONS), PARSING_STRICTNESS_1) + # Cached project. + _PROJECT = project + # Convert version suffix to an identifier element. identifier = identifier.replace('#', '.v') diff --git a/pyessv/_parsers/directory.py b/pyessv/_parsers/directory.py index f85eebf..c82da66 100644 --- a/pyessv/_parsers/directory.py +++ b/pyessv/_parsers/directory.py @@ -24,7 +24,16 @@ _TEST_DIRECTORY = 'CMIP5/output1/IPSL/IPSL-CM5A-LR/1pctCO2/mon/atmos/Amon/r1i1p1/v20110427/tas' _TEST_DIRECTORY = 'CMIP5/output1/IPSL/IPSL-CM5A-LR/1pctCO2/mon/atmos/Amon/r1i1p1/latest/tas' -# Instantiated & cached parser instance. +# Instantiated template +_TEMPLATE = None + +# Instantiated template collections +_COLLECTIONS = None + +# Instantiated project. +_PROJECT = None + +# Instantiated parser. _PARSER = None @@ -60,32 +69,38 @@ def parse_directory(project, directory): assert isinstance(project, basestring), 'Invalid project' assert isinstance(directory, basestring), 'Invalid directory' - # Instantiated template - _TEMPLATE = None + global _PROJECT, _PARSER, _TEMPLATE, _COLLECTIONS - # Instantiated template collections - _COLLECTIONS = None + if _PROJECT != project: - # Get scope corresponding to the project code. - scopes = all_scopes() - assert project in [scope.name for scope in scopes], 'Unsupported project' - scope = [scope for scope in scopes if scope.name == project][0] + # Instantiated template + _TEMPLATE = None - # Get template from data scope. - assert 'directory_template' in scope.data.keys(), 'Directory template not found' - _TEMPLATE = scope.data['directory_template'] - assert isinstance(_TEMPLATE, basestring), 'Invalid template' + # Instantiated template collections + _COLLECTIONS = None - # Get template collections from data scope. - assert 'directory_collections' in scope.data.keys(), 'Template collections not found' - _COLLECTIONS = list() - for name in scope.data['directory_collections']: - _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) - assert _COLLECTIONS, 'Invalid collections' + # Get scope corresponding to the project code. + scopes = all_scopes() + assert project in [scope.name for scope in scopes], 'Unsupported project' + scope = [scope for scope in scopes if scope.name == project][0] - # Instantiate parser JIT. - global _PARSER - if _PARSER is None: + # Get template from data scope. + assert 'directory_template' in scope.data.keys(), 'Directory template not found' + _TEMPLATE = scope.data['directory_template'] + assert isinstance(_TEMPLATE, basestring), 'Invalid template' + + # Get template collections from data scope. + assert 'directory_collections' in scope.data.keys(), 'Template collections not found' + _COLLECTIONS = list() + for name in scope.data['directory_collections']: + _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) + assert _COLLECTIONS, 'Invalid collections' + + # Instantiate parser JIT. + global _PARSER _PARSER = create_template_parser(_TEMPLATE, tuple(_COLLECTIONS), PARSING_STRICTNESS_1, separator='/') + # Cached project. + _PROJECT = project + return _PARSER.parse(directory) diff --git a/pyessv/_parsers/filename.py b/pyessv/_parsers/filename.py index eeb34bd..ba0a510 100644 --- a/pyessv/_parsers/filename.py +++ b/pyessv/_parsers/filename.py @@ -26,7 +26,16 @@ _TEST_FILENAME = 'orog_fx_IPSL-CM5A-LR_1pctCO2_r0i0p0.nc' _TEST_FILENAME = 'tas_Amon_IPSL-CM5A-LR_1pctCO2_r1i1p1_185001-198912-clim.nc' -# Instantiated & cached parser instance. +# Instantiated template +_TEMPLATE = None + +# Instantiated template collections +_COLLECTIONS = None + +# Instantiated project. +_PROJECT = None + +# Instantiated parser. _PARSER = None @@ -62,34 +71,33 @@ def parse_filename(project, filename): assert isinstance(project, basestring), 'Invalid project' assert isinstance(filename, basestring), 'Invalid filename' - # Instantiated template - _TEMPLATE = None + global _PROJECT, _PARSER, _TEMPLATE, _COLLECTIONS - # Instantiated template collections - _COLLECTIONS = None + if _PROJECT != project: - # Get scope corresponding to the project code. - scopes = all_scopes() - assert project in [scope.name for scope in scopes], 'Unsupported project' - scope = [scope for scope in scopes if scope.name == project][0] + # Get scope corresponding to the project code. + scopes = all_scopes() + assert project in [scope.name for scope in scopes], 'Unsupported project' + scope = [scope for scope in scopes if scope.name == project][0] - # Get template from data scope. - assert 'filename_template' in scope.data.keys(), 'Filename template not found' - _TEMPLATE = scope.data['filename_template'] - assert isinstance(_TEMPLATE, basestring), 'Invalid template' + # Get template from data scope. + assert 'filename_template' in scope.data.keys(), 'Filename template not found' + _TEMPLATE = scope.data['filename_template'] + assert isinstance(_TEMPLATE, basestring), 'Invalid template' - # Get template collections from data scope. - assert 'filename_collections' in scope.data.keys(), 'Filename collections not found' - _COLLECTIONS = list() - for name in scope.data['filename_collections']: - _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) - assert _COLLECTIONS, 'Invalid collections' + # Get template collections from data scope. + assert 'filename_collections' in scope.data.keys(), 'Filename collections not found' + _COLLECTIONS = list() + for name in scope.data['filename_collections']: + _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) + assert _COLLECTIONS, 'Invalid collections' - # Instantiate parser JIT. - global _PARSER - if _PARSER is None: + # Instantiate parser JIT. _PARSER = create_template_parser(_TEMPLATE, tuple(_COLLECTIONS), PARSING_STRICTNESS_1, separator='_') + # Cached project. + _PROJECT = project + # Strip file extension. filename = splitext(filename)[0] From a1bef02d218014d06bb88cc8dbffa4aa960a062a Mon Sep 17 00:00:00 2001 From: Guillaume Date: Thu, 4 Apr 2019 16:43:42 +0200 Subject: [PATCH 06/14] Update ini map writers --- pyessv/_parsers/dataset_id.py | 2 +- sh/writers/esgf/map_c3s_cordex.py | 2 +- sh/writers/esgf/map_cordex.py | 4 ++-- sh/writers/esgf/map_cordex_adjust.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyessv/_parsers/dataset_id.py b/pyessv/_parsers/dataset_id.py index bda091d..9850120 100644 --- a/pyessv/_parsers/dataset_id.py +++ b/pyessv/_parsers/dataset_id.py @@ -19,7 +19,7 @@ # Test identifier (for reference purpose only). _TEST_CMIP5_IDENTIFIER = 'cmip5.output2.IPSL.IPSL-CM5A-LR.historicalMisc.mon.ocean.Omon.r2i1p1.v20150504' -_TEST_CORDEX_IDENTIFIER = 'cordex.output.AFR-44.MOHC.MOHC-HadGEM2-ES.rcp60.r12i1p1.hadgem3-ra.v1.mon.areacella' +_TEST_CORDEX_IDENTIFIER = 'cordex.output.AFR-44.MOHC.MOHC-HadGEM2-ES.rcp60.r12i1p1.HadGEM3-RA.v1.mon.areacella#20190804' # Instantiated template _TEMPLATE = None diff --git a/sh/writers/esgf/map_c3s_cordex.py b/sh/writers/esgf/map_c3s_cordex.py index 58fea71..eee830b 100644 --- a/sh/writers/esgf/map_c3s_cordex.py +++ b/sh/writers/esgf/map_c3s_cordex.py @@ -73,7 +73,7 @@ 'rcm_version', 'time_frequency', 'variable', - 'version' + 'dataset_version' ) } diff --git a/sh/writers/esgf/map_cordex.py b/sh/writers/esgf/map_cordex.py index 5ee5abe..c990906 100644 --- a/sh/writers/esgf/map_cordex.py +++ b/sh/writers/esgf/map_cordex.py @@ -31,7 +31,7 @@ ('thredds_exclude_variables', yield_comma_delimited_options), ('time_frequency', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), - ('dataset_version', r'latest|v^[0-9]*$'), + ('dataset_version', r'latest|^v[0-9]*$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') } @@ -75,7 +75,7 @@ 'rcm_version', 'time_frequency', 'variable', - 'version' + 'dataset_version' ) } diff --git a/sh/writers/esgf/map_cordex_adjust.py b/sh/writers/esgf/map_cordex_adjust.py index afe1810..c008550 100644 --- a/sh/writers/esgf/map_cordex_adjust.py +++ b/sh/writers/esgf/map_cordex_adjust.py @@ -75,7 +75,7 @@ 'bias_adjustment', 'time_frequency', 'variable', - 'version' + 'dataset_version' ) } From 853765031f4621357e05cd6b69800b2af696b3a3 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Mon, 8 Apr 2019 17:53:46 +0200 Subject: [PATCH 07/14] Remove unused global delcaration --- pyessv/_parser_template.py | 2 +- pyessv/_parsers/dataset_id.py | 1 - pyessv/_parsers/directory.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pyessv/_parser_template.py b/pyessv/_parser_template.py index 95b136e..ed5dd9e 100644 --- a/pyessv/_parser_template.py +++ b/pyessv/_parser_template.py @@ -63,7 +63,7 @@ def parse(self, val): # Verify constant match. if isinstance(template_part, basestring): if template_part != name: - raise TemplateParsingError('{} :: {}'.format(name, val)) + raise TemplateValueError('{} :: {}'.format(name, val)) continue # Verify collection match. diff --git a/pyessv/_parsers/dataset_id.py b/pyessv/_parsers/dataset_id.py index 9850120..1c73555 100644 --- a/pyessv/_parsers/dataset_id.py +++ b/pyessv/_parsers/dataset_id.py @@ -88,7 +88,6 @@ def parse_dataset_identifier(project, identifier): assert _COLLECTIONS, 'Invalid collections' # Instantiate parser JIT. - global _PARSER _PARSER = create_template_parser(_TEMPLATE, tuple(_COLLECTIONS), PARSING_STRICTNESS_1) # Cached project. diff --git a/pyessv/_parsers/directory.py b/pyessv/_parsers/directory.py index c82da66..74d3138 100644 --- a/pyessv/_parsers/directory.py +++ b/pyessv/_parsers/directory.py @@ -97,7 +97,6 @@ def parse_directory(project, directory): assert _COLLECTIONS, 'Invalid collections' # Instantiate parser JIT. - global _PARSER _PARSER = create_template_parser(_TEMPLATE, tuple(_COLLECTIONS), PARSING_STRICTNESS_1, separator='/') # Cached project. From 4d56e7abfe0e8754aefa771c19903d4f716139ff Mon Sep 17 00:00:00 2001 From: Guillaume Date: Tue, 9 Apr 2019 10:00:20 +0200 Subject: [PATCH 08/14] Disable log message at import. --- pyessv/_initializer.py | 4 ++-- pyessv/_ws/utils/config_loader.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyessv/_initializer.py b/pyessv/_initializer.py index 72365a7..18812f3 100644 --- a/pyessv/_initializer.py +++ b/pyessv/_initializer.py @@ -45,11 +45,11 @@ def _load_authorities(): """Loads vocabulary authorities from archive. """ - logger.log('Loading vocabularies from {}:'.format(DIR_ARCHIVE)) + #ogger.log('Loading vocabularies from {}:'.format(DIR_ARCHIVE)) authorities = [] for authority in read(): authorities.append(authority) - logger.log('... loaded: {}'.format(authority)) + #logger.log('... loaded: {}'.format(authority)) cache(authority) return authorities diff --git a/pyessv/_ws/utils/config_loader.py b/pyessv/_ws/utils/config_loader.py index 37d276d..b583131 100644 --- a/pyessv/_ws/utils/config_loader.py +++ b/pyessv/_ws/utils/config_loader.py @@ -58,8 +58,8 @@ def _init(): if fpath is not None: data = json_file_to_namedtuple(fpath) logger.log_web("Config file loaded @ {}".format(fpath)) - else: - logger.log_web_warning("Web-service config file not found: reverted to default") + #else: + # logger.log_web_warning("Web-service config file not found: reverted to default") # Auto-initialize. From 80d96829d474a76421b8e65a12b797a73941c54e Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 10 Apr 2019 16:10:19 +0200 Subject: [PATCH 09/14] Add template builder feature. --- pyessv/__init__.py | 2 + pyessv/_builder_template.py | 84 ++++++++++++++++++++++++ pyessv/_builders/__init__.py | 15 +++++ pyessv/_builders/dataset_id.py | 79 ++++++++++++++++++++++ pyessv/_constants.py | 9 +++ pyessv/_factory.py | 25 +++++++ pyessv/_parser_template.py | 12 ++-- pyessv/_parsers/cmip6_dataset_id.py | 53 --------------- pyessv/_parsers/cordex_dataset_id.py | 57 ---------------- pyessv/_parsers/input4mips_dataset_id.py | 53 --------------- 10 files changed, 220 insertions(+), 169 deletions(-) create mode 100644 pyessv/_builder_template.py create mode 100644 pyessv/_builders/__init__.py create mode 100644 pyessv/_builders/dataset_id.py delete mode 100644 pyessv/_parsers/cmip6_dataset_id.py delete mode 100644 pyessv/_parsers/cordex_dataset_id.py delete mode 100644 pyessv/_parsers/input4mips_dataset_id.py diff --git a/pyessv/__init__.py b/pyessv/__init__.py index c128667..24a943a 100644 --- a/pyessv/__init__.py +++ b/pyessv/__init__.py @@ -72,6 +72,8 @@ from pyessv._model import Scope from pyessv._model import Term +from pyessv._builders import build_dataset_identifier + from pyessv._parser import parse from pyessv._parsers import parse_dataset_identifier from pyessv._parsers import parse_dataset_identifiers diff --git a/pyessv/_builder_template.py b/pyessv/_builder_template.py new file mode 100644 index 0000000..917dc36 --- /dev/null +++ b/pyessv/_builder_template.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- + +""" +.. module:: pyessv._model._builder_template.py + :copyright: Copyright "December 01, 2016", IPSL + :license: GPL/CeCIL + :platform: Unix, Windows + :synopsis: A vocabulary constrained template builder, e.g. a dataset identifier. + +.. moduleauthor:: Mark Conway-Greenslade + + +""" +from pyessv._model import Collection, Term +from pyessv._exceptions import TemplateParsingError, TemplateValueError +from pyessv._constants import BUILDER_FIELDS + +class TemplateBuilder(object): + """A vocabulary template builder. + + """ + def __init__(self, template, collections, strictness, separator='.'): + """Instance constructor. + + :param str template: Identifier template. + :param tuple collections: pyessv collection identifiers. + :param int strictness: Strictness level to apply when applying name matching rules. + :param str seprarator: Separator to apply when parsing. + + """ + from pyessv._loader import load + + self.separator = separator + self.template_parts = template.split(separator) + self.template = template + self.strictness = strictness + + # Inject pyessv collections into template. + collection_idx = 0 + for idx, part in enumerate(self.template_parts): + if part == '{}': + self.template_parts[idx] = load(collections[collection_idx]) + collection_idx += 1 + + + def build(self, terms, att='label', alt_name=0): + """Build template instance from a list of pyessv terms. + + :returns: Template instance string. + + """ + assert isinstance(alt_name, int), 'Invalid alternative name index' + assert att in BUILDER_FIELDS, 'Invalid name' + + # Instantiate string parts. + string_parts = list() + + # Iterate template. + for template_part in self.template_parts: + + # Append constant match. + if isinstance(template_part, basestring): + string_parts.append(template_part) + continue + + # Append term match. + collection = template_part + term = None + for term in terms: + if term.collection == collection: + break + + # Verify collection is found among terms. + if not term: + raise TemplateValueError('Collection not found among terms :: {}'.format(collection)) + + # Get term field. + if att == 'alternative_names': + string_parts.append(getattr(term, att)[alt_name]) + else: + string_parts.append(getattr(term, att)) + + + return self.separator.join(string_parts) \ No newline at end of file diff --git a/pyessv/_builders/__init__.py b/pyessv/_builders/__init__.py new file mode 100644 index 0000000..621f769 --- /dev/null +++ b/pyessv/_builders/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +""" +.. module:: pyessv._builders.__init__.py + :copyright: Copyright "December 01, 2016", IPSL + :license: GPL/CeCIL + :platform: Unix, Windows + :synopsis: Expression builders. + +.. moduleauthor:: Mark Conway-Greenslade + + +""" + +from pyessv._builders.dataset_id import build_dataset_identifier diff --git a/pyessv/_builders/dataset_id.py b/pyessv/_builders/dataset_id.py new file mode 100644 index 0000000..57a45d4 --- /dev/null +++ b/pyessv/_builders/dataset_id.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- + +""" +.. module:: pyessv._parsers.dataset_id.py + :copyright: Copyright "December 01, 2016", IPSL + :license: GPL/CeCIL + :platform: Unix, Windows + :synopsis: Encapsulates parsing of an ESGF dataset identifier. + +.. moduleauthor:: Mark Conway-Greenslade + +""" +from pyessv._model.term import Term +from pyessv import all_scopes +from pyessv._constants import PARSING_STRICTNESS_1 +from pyessv._factory import create_template_builder +from pyessv._utils.compat import basestring + +# Test identifier (for reference purpose only). +_TEST_CMIP5_IDENTIFIER = 'cmip5.output2.IPSL.IPSL-CM5A-LR.historicalMisc.mon.ocean.Omon.r2i1p1.v20150504' +_TEST_CORDEX_IDENTIFIER = 'cordex.output.AFR-44.MOHC.MOHC-HadGEM2-ES.rcp60.r12i1p1.HadGEM3-RA.v1.mon.areacella#20190804' + +# Instantiated template +_TEMPLATE = None + +# Instantiated template collections +_COLLECTIONS = None + +# Instantiated project. +_PROJECT = None + +# Instantiated builder. +_BUILDER = None + + +def build_dataset_identifier(project, terms): + """Builds a dataset identifier. + + :param str project: Project code. + :param set terms: Dataset identifier terms. + + :returns: Dataset identifier. + :rtype: str + + """ + assert isinstance(project, basestring), 'Invalid project' + assert isinstance(terms, set), 'Invalid terms' + + global _PROJECT, _BUILDER, _TEMPLATE, _COLLECTIONS + + if _PROJECT != project: + + # Get scope corresponding to the project code. + scopes = all_scopes() + assert project in [scope.name for scope in scopes], 'Unsupported project' + scope = [scope for scope in scopes if scope.name == project][0] + + # Get template from data scope. + assert 'dataset_id_template' in scope.data.keys(), 'Dataset ID template not found' + _TEMPLATE = scope.data['dataset_id_template'] + assert isinstance(_TEMPLATE, basestring), 'Invalid template' + + # Get template collections from data scope. + assert 'dataset_id_collections' in scope.data.keys(), 'Template collections not found' + _COLLECTIONS = list() + for name in scope.data['dataset_id_collections']: + _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) + assert _COLLECTIONS, 'Invalid collections' + + # Instantiate parser JIT. + _BUILDER = create_template_builder(_TEMPLATE, tuple(_COLLECTIONS), PARSING_STRICTNESS_1) + + # Cached project. + _PROJECT = project + + for term in terms: + assert isinstance(term, Term), 'Invalid term :: {}'.format(term) + + return _BUILDER.build(terms) diff --git a/pyessv/_constants.py b/pyessv/_constants.py index 8b04660..239e6f2 100644 --- a/pyessv/_constants.py +++ b/pyessv/_constants.py @@ -105,6 +105,15 @@ # Regular expression for validating a canonical name. REGEX_CANONICAL_NAME = r'^[a-z0-9\-]*$' +# Builder fields. +BUILDER_FIELDS = ( + 'name', + 'canonical_name', + 'raw_name', + 'alternative_names', + 'label' + ) + # Standard node fields. STANDARD_NODE_FIELDS = ( 'alternative_names', diff --git a/pyessv/_factory.py b/pyessv/_factory.py index 69f835e..62f95db 100644 --- a/pyessv/_factory.py +++ b/pyessv/_factory.py @@ -23,6 +23,7 @@ from pyessv._model import Scope from pyessv._model import Term from pyessv._parser_template import TemplateParser +from pyessv._builder_template import TemplateBuilder from pyessv._utils.compat import basestring from pyessv._utils.compat import str from pyessv._utils.formatter import format_canonical_name @@ -197,6 +198,30 @@ def _callback(instance): ) +def create_template_builder(template, collections, strictness=PARSING_STRICTNESS_2, separator='.'): + """Instantiates, initialises & returns a template builder. + + :param str template: An expression template. + :param tuple collections: Collections that the template maps to. + :param int strictness: Strictness level to apply when applying name matching rules. + :param str separator: Separator to apply when parsing. + + :returns: A vocabulary expression builder. + :rtype: pyessv.TemplateBuilder + + """ + assert isinstance(template, basestring), 'Invalid template' + assert isinstance(collections, tuple), 'Invalid collections' + assert len(template) > 0, 'Invalid template' + assert template.count('{}') > 0, 'Invalid template' + assert len(collections) > 0, 'Invalid collections' + assert template.count('{}') == len(collections), 'Invalid template: collection count mismatch' + assert strictness in PARSING_STRICTNESS_SET, 'Invalid parsing strictness: {}'.format(strictness) + assert isinstance(separator, basestring), 'Invalid separator' + + return TemplateBuilder(template, collections, strictness, separator) + + def create_template_parser(template, collections, strictness=PARSING_STRICTNESS_2, separator='.'): """Instantiates, initialises & returns a template parser. diff --git a/pyessv/_parser_template.py b/pyessv/_parser_template.py index ed5dd9e..ccc73b7 100644 --- a/pyessv/_parser_template.py +++ b/pyessv/_parser_template.py @@ -11,22 +11,22 @@ """ +import pyessv from pyessv._exceptions import TemplateParsingError, TemplateValueError -from pyessv._model.collection import Collection from pyessv._model.term import Term -from pyessv._utils.compat import basestring -import pyessv +from pyessv._utils.compat import basestring class TemplateParser(object): """A vocabulary template parser. """ + def __init__(self, template, collections, strictness, separator='.'): """Instance constructor. :param str template: Identifier template. - :param list collections: pyessv collection identifiers. + :param tuple collections: pyessv collection identifiers. :param int strictness: Strictness level to apply when applying name matching rules. :param str seprarator: Separator to apply when parsing. @@ -45,7 +45,6 @@ def __init__(self, template, collections, strictness, separator='.'): self.template_parts[idx] = load(collections[collection_idx]) collection_idx += 1 - def parse(self, val): """Parses a val against a template. @@ -70,7 +69,8 @@ def parse(self, val): collection = template_part term = collection.is_matched(name, self.strictness) if term == False: - raise TemplateValueError('vocab={} :: strictness={} :: value={}'.format(collection, self.strictness, name)) + raise TemplateValueError( + 'vocab={} :: strictness={} :: value={}'.format(collection, self.strictness, name)) # Create a virtual term if needed. if isinstance(term, Term): diff --git a/pyessv/_parsers/cmip6_dataset_id.py b/pyessv/_parsers/cmip6_dataset_id.py deleted file mode 100644 index 34f39c8..0000000 --- a/pyessv/_parsers/cmip6_dataset_id.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -.. module:: pyessv._parsers.cmip6_dataset_id.py - :copyright: Copyright "December 01, 2016", IPSL - :license: GPL/CeCIL - :platform: Unix, Windows - :synopsis: Encapsulates parsing of a CMIP6 dataset identifier. - -.. moduleauthor:: Mark Conway-Greenslade - -""" -from pyessv._factory import create_template_parser -from pyessv._constants import PARSING_STRICTNESS_1 - - - -# Template extracted from esgf ini file (for reference purpose only). -_INI_PATTERN = 'CMIP6.%(activity_id)s.%(institution_id)s.%(source_id)s.%(experiment_id)s.%(member_id)s.%(table_id)s.%(variable_id)s.%(grid_label)s' - -# Template that identifiers must conform to. -_TEMPLATE = 'CMIP6.{}.{}.{}.{}.{}.{}.{}.{}.{}' - -# Collections injected into template. -_COLLECTIONS = ( - 'wcrp:cmip6:activity-id', - 'wcrp:cmip6:institution-id', - 'wcrp:cmip6:source-id', - 'wcrp:cmip6:experiment-id', - 'wcrp:cmip6:member-id', - 'wcrp:cmip6:table-id', - 'wcrp:cmip6:variable-id', - 'wcrp:cmip6:grid-label', - 'wcrp:cmip6:version' - ) - -# Instantiated & cached parser instance. -_PARSER = None - - -def parse(identifier): - """Parses a CMIP6 dataset identifier. - - """ - # Instantiate parser JIT. - global _PARSER - if _PARSER is None: - _PARSER = create_template_parser(_TEMPLATE, _COLLECTIONS, PARSING_STRICTNESS_1) - - # Convert version suffix to an identifier element. - identifier = identifier.replace('#', '.v') - - return _PARSER.parse(identifier) diff --git a/pyessv/_parsers/cordex_dataset_id.py b/pyessv/_parsers/cordex_dataset_id.py deleted file mode 100644 index 28d280b..0000000 --- a/pyessv/_parsers/cordex_dataset_id.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -.. module:: pyessv._parsers.cordex_dataset_id.py - :copyright: Copyright "December 01, 2016", IPSL - :license: GPL/CeCIL - :platform: Unix, Windows - :synopsis: Encapsulates parsing of a CORDEX dataset identifier. - -.. moduleauthor:: Mark Conway-Greenslade - -""" -from pyessv._factory import create_template_parser -from pyessv._constants import PARSING_STRICTNESS_1 - - - -# Template extracted from esgf ini file (for reference purpose only). -_INI_PATTERN = 'cordex.%(product)s.%(domain)s.%(institute)s.%(driving_model)s.%(experiment)s.%(ensemble)s.%(rcm_name)s.%(rcm_version)s.%(time_frequency)s.%(variable)s' - -# Test identifier (for reference purpose only). -_TEST_IDENTIFIER = 'cordex.output.AFR-44.MOHC.MOHC-HadGEM2-ES.rcp60.r12i1p1.hadgem3-ra.v1.mon.areacella' - -# Template that identifiers must conform to. -_TEMPLATE = 'cordex.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}' - -# Collections injected into template. -_COLLECTIONS = ( - 'wcrp:cordex:product', - 'wcrp:cordex:domain', - 'wcrp:cordex:institute', - 'wcrp:cordex:driving-model', - 'wcrp:cordex:experiment', - 'wcrp:cordex:ensemble', - 'wcrp:cordex:rcm-name', - 'wcrp:cordex:rcm-version', - 'wcrp:cordex:time-frequency', - 'wcrp:cordex:variable', - 'wcrp:cordex:version' - ) - -# Instantiated & cached parser instance. -_PARSER = None - -def parse(identifier): - """Parses a CORDEX dataset identifier. - - """ - # Instantiate parser JIT. - global _PARSER - if _PARSER is None: - _PARSER = create_template_parser(_TEMPLATE, _COLLECTIONS, PARSING_STRICTNESS_1) - - # Convert version suffix to an identifier element. - identifier = identifier.replace('#', '.v') - - return _PARSER.parse(identifier) diff --git a/pyessv/_parsers/input4mips_dataset_id.py b/pyessv/_parsers/input4mips_dataset_id.py deleted file mode 100644 index f01a245..0000000 --- a/pyessv/_parsers/input4mips_dataset_id.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -.. module:: pyessv._parsers.cmip6_dataset_id.py - :copyright: Copyright "December 01, 2016", IPSL - :license: GPL/CeCIL - :platform: Unix, Windows - :synopsis: Encapsulates parsing of a CMIP6 dataset identifier. - -.. moduleauthor:: Mark Conway-Greenslade - -""" -from pyessv._factory import create_template_parser -from pyessv._constants import PARSING_STRICTNESS_3 - - - -# Template extracted from esgf ini file (for reference purpose only). -_INI_PATTERN = '%(activity_id)s.%(mip_era)s.%(target_mip)s.%(institution_id)s.%(source_id)s.%(realm)s.%(frequency)s.%(variable_id)s.%(grid_label)s' - -# Template that identifiers must conform to. -_TEMPLATE = 'input4MIPs.CMIP6.{}.{}.{}.{}.{}.{}.{}' - -# Collections injected into template. -_COLLECTIONS = ( - 'wcrp:input4mips:target-mip', - 'wcrp:input4mips:institution-id', - 'wcrp:input4mips:source-id', - 'wcrp:input4mips:realm', - 'wcrp:input4mips:frequency', - 'wcrp:input4mips:variable-id', - 'wcrp:input4mips:grid-label', - ) - -# Instantiated & cached parser instance. -_PARSER = None - - -def parse(identifier): - """Parses a input4MIPs dataset identifier. - - """ - print identifier - # Instantiate parser JIT. - global _PARSER - if _PARSER is None: - _PARSER = create_template_parser(_TEMPLATE, _COLLECTIONS, PARSING_STRICTNESS_3) - - # Strip version suffix. - if '#' in identifier: - identifier = identifier.split('#')[0] - - return _PARSER.parse(identifier) From 60274c8f1fc9e5c34e062775c4278fd843077b6a Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 10 Apr 2019 19:08:58 +0200 Subject: [PATCH 10/14] Add associations to writer + Change initializer to avoid all authority CV loading at import --- pyessv/__init__.py | 1 + pyessv/_initializer.py | 37 ++++++++++++++++++++------------- pyessv/_io_manager.py | 8 +++++-- sh/writers/esgf/map.py | 8 ++++++- sh/writers/esgf/map_cmip5.py | 7 ++++--- sh/writers/esgf/map_obs4mips.py | 2 +- 6 files changed, 42 insertions(+), 21 deletions(-) diff --git a/pyessv/__init__.py b/pyessv/__init__.py index 24a943a..b5af97c 100644 --- a/pyessv/__init__.py +++ b/pyessv/__init__.py @@ -62,6 +62,7 @@ from pyessv._governance import reset from pyessv._initializer import init +from pyessv._initializer import load_cv from pyessv._loader import load_random from pyessv._loader import load diff --git a/pyessv/_initializer.py b/pyessv/_initializer.py index 18812f3..1a89df5 100644 --- a/pyessv/_initializer.py +++ b/pyessv/_initializer.py @@ -31,8 +31,17 @@ def init(): if not os.path.isdir(DIR_ARCHIVE): raise EnvironmentError('{} directory does not exists'.format(DIR_ARCHIVE)) - # Load set of authorities from file system. - authorities = _load_authorities() + +def load_cv(authority=None): + authorities = [] + if authority: + authority = read(authority=authority) + authorities.append(authority) + cache(authority) + else: + for authority in read(): + authorities.append(authority) + cache(authority) # Mixin pseudo-constants. _mixin_constants(authorities) @@ -41,18 +50,18 @@ def init(): _mixin_scope_accessors(authorities) -def _load_authorities(): - """Loads vocabulary authorities from archive. - - """ - #ogger.log('Loading vocabularies from {}:'.format(DIR_ARCHIVE)) - authorities = [] - for authority in read(): - authorities.append(authority) - #logger.log('... loaded: {}'.format(authority)) - cache(authority) - - return authorities +# def _load_authorities(): +# """Loads vocabulary authorities from archive. +# +# """ +# logger.log('Loading vocabularies from {}:'.format(DIR_ARCHIVE)) +# authorities = [] +# for authority in read(): +# authorities.append(authority) +# logger.log('... loaded: {}'.format(authority)) +# cache(authority) +# +# return authorities def _mixin_constants(authorities): diff --git a/pyessv/_io_manager.py b/pyessv/_io_manager.py index 4ec11ba..8d6c028 100644 --- a/pyessv/_io_manager.py +++ b/pyessv/_io_manager.py @@ -71,14 +71,18 @@ def delete(target): pass -def read(archive_dir=DIR_ARCHIVE): +def read(archive_dir=DIR_ARCHIVE, authority=None): """Reads vocabularies from archive folder (~/.esdoc/pyessv-archive) upon file system. :returns: List of vocabulary authorities loaded from archive folder. :rtype: list """ - return [_read_authority(i) for i in glob.glob('{}/*'.format(archive_dir)) if isdir(i)] + if authority: + assert '{}/{}'.format(archive_dir, authority), 'Invalid authority' + return _read_authority('{}/{}'.format(archive_dir, authority)) + else: + return [_read_authority(i) for i in glob.glob('{}/*'.format(archive_dir)) if isdir(i)] def _read_authority(dpath): diff --git a/sh/writers/esgf/map.py b/sh/writers/esgf/map.py index 019e5ad..6edbcc7 100644 --- a/sh/writers/esgf/map.py +++ b/sh/writers/esgf/map.py @@ -123,7 +123,13 @@ def _main(args): except TypeError: pass for term_data in term_factory(ctx): - _get_term(collection, term_data) + try: + term_src, term_dst = term_data + t = _get_term(collection, term_dst) + s = pyessv.load(term_src) + s.associations.append(t) + except (ValueError, AttributeError): + _get_term(collection, term_data) # Add to archive & persist to file system. pyessv.archive(authority) diff --git a/sh/writers/esgf/map_cmip5.py b/sh/writers/esgf/map_cmip5.py index ca17f41..151f8a2 100644 --- a/sh/writers/esgf/map_cmip5.py +++ b/sh/writers/esgf/map_cmip5.py @@ -21,9 +21,9 @@ ('cmor_table', yield_comma_delimited_options), ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), ('experiment', yield_pipe_delimited_options), + ('model', yield_comma_delimited_options), ('institute', lambda: yield_institute), ('las_time_delta', lambda: yield_las_time_delta), - ('model', yield_comma_delimited_options), ('time_frequency', yield_comma_delimited_options), ('product', yield_comma_delimited_options), ('realm', yield_comma_delimited_options), @@ -88,8 +88,9 @@ def yield_institute(ctx): """Yields institute information to be converted to pyessv terms. """ - for _, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): - yield institute + for model, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): + src_namespace = 'wcrp:cmip5:model:{}'.format(model.lower().replace('_','-')) + yield (src_namespace, institute) def yield_las_time_delta(ctx): diff --git a/sh/writers/esgf/map_obs4mips.py b/sh/writers/esgf/map_obs4mips.py index e882fa3..8ce733c 100644 --- a/sh/writers/esgf/map_obs4mips.py +++ b/sh/writers/esgf/map_obs4mips.py @@ -19,8 +19,8 @@ ('product', yield_comma_delimited_options), ('institute', yield_comma_delimited_options), ('realm', yield_comma_delimited_options), - ('variable', lambda: yield_variable), ('var', yield_comma_delimited_options), + ('variable', lambda: yield_variable), ('time_frequency', yield_comma_delimited_options), ('data_structure', yield_comma_delimited_options), ('source_id', yield_comma_delimited_options), From c1bdd12b166a3b6009547a96ee04e19fa5fe3d26 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Mon, 15 Apr 2019 15:16:43 +0200 Subject: [PATCH 11/14] Complete associations between terms. --- sh/writers/esgf/map.py | 28 +++-- sh/writers/esgf/map_c3s_cmip5.py | 129 +++++++++++----------- sh/writers/esgf/map_c3s_cordex.py | 134 +++++++++++------------ sh/writers/esgf/map_cc4e.py | 97 +++++++++-------- sh/writers/esgf/map_cmip5.py | 143 +++++++++++++------------ sh/writers/esgf/map_cmip6.py | 101 ++++++++++-------- sh/writers/esgf/map_cordex.py | 145 +++++++++++++------------ sh/writers/esgf/map_cordex_adjust.py | 145 +++++++++++++------------ sh/writers/esgf/map_euclipse.py | 99 +++++++++-------- sh/writers/esgf/map_geomip.py | 100 +++++++++-------- sh/writers/esgf/map_input4mips.py | 98 +++++++++-------- sh/writers/esgf/map_isimip_ft.py | 153 ++++++++++++++------------- sh/writers/esgf/map_lucid.py | 100 +++++++++-------- sh/writers/esgf/map_obs4mips.py | 10 +- sh/writers/esgf/map_pmip3.py | 99 +++++++++-------- sh/writers/esgf/map_primavera.py | 108 ++++++++++--------- sh/writers/esgf/map_tamip.py | 100 +++++++++-------- 17 files changed, 946 insertions(+), 843 deletions(-) diff --git a/sh/writers/esgf/map.py b/sh/writers/esgf/map.py index 6edbcc7..d7bc035 100644 --- a/sh/writers/esgf/map.py +++ b/sh/writers/esgf/map.py @@ -75,6 +75,9 @@ def _main(args): if not os.path.isdir(args.source): raise ValueError('ESGF vocab directory does not exist: {}'.format(args.source)) + # Load vocabulary. + pyessv.load_cv() + # CV authority = ECMWF. #_AUTHORITY = pyessv.create_authority( # 'ECMWF', @@ -83,7 +86,6 @@ def _main(args): # url='https://www.ecmwf.int/', # create_date=_CREATE_DATE #) - # Process project modules: for module in _MODULES: # Set project. @@ -127,7 +129,10 @@ def _main(args): term_src, term_dst = term_data t = _get_term(collection, term_dst) s = pyessv.load(term_src) - s.associations.append(t) + if t not in s.associations: + s.associations.append(t) + if s not in t.associations: + t.associations.append(s) except (ValueError, AttributeError): _get_term(collection, term_data) @@ -201,7 +206,7 @@ def _create_collection(module, scope, collection_id, term_regex=None): except (AttributeError, KeyError): data = None if collection_id.lower().replace('_', '-') in [collection.name for collection in scope.collections]: - collection = scope[collection_id] + collection = scope[collection_id.lower().replace('_', '-')] collection.description = "ESGF publisher-config CV collection: ".format(collection_id), collection.label = collection_id.title().replace('_', ' ').replace('Rcm', 'RCM').replace('Cmor', 'CMOR') collection.term_regex = term_regex @@ -240,11 +245,18 @@ def _get_term(collection, term_info): alternative_names = [] if synonym is None else [synonym] - return pyessv.create_term(collection, name, - label=label, - description=description, - alternative_names=alternative_names - ) + if name.lower().replace('_', '-') in [term.name for term in collection.terms]: + term = collection[name.lower().replace('_', '-')] + term.label = label + term.description = description + term.alternative_names = alternative_names + return term + else: + return pyessv.create_term(collection, name, + label=label, + description=description, + alternative_names=alternative_names + ) # Entry point. diff --git a/sh/writers/esgf/map_c3s_cmip5.py b/sh/writers/esgf/map_c3s_cmip5.py index 8331474..fe2c53e 100644 --- a/sh/writers/esgf/map_c3s_cmip5.py +++ b/sh/writers/esgf/map_c3s_cmip5.py @@ -13,80 +13,85 @@ from utils import yield_pipe_delimited_options -# TODO process map: institute_map = map(model : institute) - - # Vocabulary collections extracted from ini file. COLLECTIONS = { - ('cmor_table', yield_comma_delimited_options), - ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), - ('experiment', yield_pipe_delimited_options), - ('institute', lambda: yield_institute), - ('model', yield_comma_delimited_options), - ('time_frequency', yield_comma_delimited_options), - ('product', yield_comma_delimited_options), - ('realm', yield_comma_delimited_options), - ('thredds_exclude_variables', yield_comma_delimited_options), - ('variable', yield_comma_delimited_options), - ('dataset_version', r'latest|^v[0-9]*$'), - ('file_period', r'fixed|^\d+-\d+(-clim)?$') + ('cmor_table', yield_comma_delimited_options), + ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), + ('experiment', yield_pipe_delimited_options), + ('institute', lambda: yield_institute), + ('model', yield_comma_delimited_options), + ('time_frequency', yield_comma_delimited_options), + ('product', yield_comma_delimited_options), + ('realm', yield_comma_delimited_options), + ('thredds_exclude_variables', yield_comma_delimited_options), + ('variable', yield_comma_delimited_options), + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Arbitrary data associated with a collection. COLLECTION_DATA = { - 'experiment': { - 'cim_document_type': 'cim.1.activity.NumericalExperiment', - 'cim_document_type_alternative_name': 'experiment' - }, - 'model': { - 'cim_document_type': 'cim.1.software.ModelComponent', - 'cim_document_type_alternative_name': 'model' - } + 'experiment': { + 'cim_document_type': 'cim.1.activity.NumericalExperiment', + 'cim_document_type_alternative_name': 'experiment' + }, + 'model': { + 'cim_document_type': 'cim.1.software.ModelComponent', + 'cim_document_type_alternative_name': 'model' + } } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable', - 'cmor_table', - 'model', - 'experiment', - 'ensemble', - 'file_period' - ), - 'directory_template': 'C3S-CMIP5/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'variable', - 'dataset_version' - ), - 'dataset_id_template': 'c3s-cmip5.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'variable', - 'dataset_version' - ) + 'filename': { + + 'template': '{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'C3S-CMIP5/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'variable', + 'dataset_version' + ) + }, + 'dataset_id': { + 'template': 'c3s-cmip5.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'variable', + 'dataset_version' + ) + } } def yield_institute(ctx): - """Yields institute information to be converted to pyessv terms. + """Yields institute information to be converted to pyessv terms. - """ - for _, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): - yield institute + """ + for model, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): + src_namespace = 'ecmwf:c3s-cmip5:model:{}'.format(model.lower().replace('_', '-')) + yield src_namespace, institute diff --git a/sh/writers/esgf/map_c3s_cordex.py b/sh/writers/esgf/map_c3s_cordex.py index eee830b..b5fdbb3 100644 --- a/sh/writers/esgf/map_c3s_cordex.py +++ b/sh/writers/esgf/map_c3s_cordex.py @@ -12,83 +12,87 @@ from utils import yield_comma_delimited_options from utils import yield_pipe_delimited_options - -# TODO process map: rcm_name_map = map(project, rcm_model : rcm_name) - # Vocabulary collections extracted from ini file. COLLECTIONS = { - ('domain', lambda: yield_domain), - ('driving_model', yield_comma_delimited_options), - ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), - ('experiment', yield_pipe_delimited_options), - ('institute', yield_comma_delimited_options), - ('product', yield_comma_delimited_options), - ('rcm_model', yield_comma_delimited_options), - ('rcm_name', lambda: yield_rcm_name), - ('rcm_version', yield_comma_delimited_options), - ('thredds_exclude_variables', yield_comma_delimited_options), - ('time_frequency', yield_comma_delimited_options), - ('variable', yield_comma_delimited_options), - ('dataset_version', r'latest|v^[0-9]*$'), - ('file_period', r'fixed|^\d+-\d+(-clim)?$') + ('domain', lambda: yield_domain), + ('driving_model', yield_comma_delimited_options), + ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), + ('experiment', yield_pipe_delimited_options), + ('institute', yield_comma_delimited_options), + ('product', yield_comma_delimited_options), + ('rcm_model', yield_comma_delimited_options), + ('rcm_name', lambda: yield_rcm_name), + ('rcm_version', yield_comma_delimited_options), + ('thredds_exclude_variables', yield_comma_delimited_options), + ('time_frequency', yield_comma_delimited_options), + ('variable', yield_comma_delimited_options), + ('dataset_version', r'latest|v^[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable', - 'domain', - 'driving_model', - 'experiment', - 'ensemble', - 'rcm_model', - 'rcm_version', - 'time_frequency', - 'file_period' - ), - 'directory_template': 'C3S-CORDEX/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'product', - 'domain', - 'institute', - 'driving_model', - 'experiment', - 'ensemble', - 'rcm_model', - 'rcm_version', - 'time_frequency', - 'variable', - 'dataset_version' - ), - 'dataset_id_template': 'c3s-cordex.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'product', - 'domain', - 'institute', - 'driving_model', - 'experiment', - 'ensemble', - 'rcm_name', - 'rcm_version', - 'time_frequency', - 'variable', - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable', + 'domain', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_model', + 'rcm_version', + 'time_frequency', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'C3S-CORDEX/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'product', + 'domain', + 'institute', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_model', + 'rcm_version', + 'time_frequency', + 'variable', + 'dataset_version' + ) + }, + 'dataset_id': { + 'template': 'c3s-cordex.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'product', + 'domain', + 'institute', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_name', + 'rcm_version', + 'time_frequency', + 'variable', + 'dataset_version' + ) + } } def yield_domain(ctx): - """Yields domain information to be converted to pyessv terms. + """Yields domain information to be converted to pyessv terms. - """ - for domain_name, domain_description in ctx.ini_section.get_option('domain_description_map', '\n', '|'): - yield domain_name, domain_name, domain_description + """ + for domain_name, domain_description in ctx.ini_section.get_option('domain_description_map', '\n', '|'): + yield domain_name, domain_name, domain_description def yield_rcm_name(ctx): - """Yields rcm name information to be converted to pyessv terms. + """Yields rcm name information to be converted to pyessv terms. - """ - for _, rcm_name in ctx.ini_section.get_option('rcm_name_map', '\n', '|'): - yield rcm_name + """ + for rcm_model, rcm_name in ctx.ini_section.get_option('rcm_name_map', '\n', '|'): + src_namespace = 'ecmwf:c3s-cordex:rcm_model:{}'.format(rcm_model.lower().replace('_', '-')) + yield src_namespace, rcm_name diff --git a/sh/writers/esgf/map_cc4e.py b/sh/writers/esgf/map_cc4e.py index 2390d85..4052980 100644 --- a/sh/writers/esgf/map_cc4e.py +++ b/sh/writers/esgf/map_cc4e.py @@ -11,56 +11,61 @@ """ from utils import yield_comma_delimited_options - # Vocabulary collections extracted from ini file. COLLECTIONS = { - ('work_package', yield_comma_delimited_options), - ('product', yield_comma_delimited_options), - ('source_type', yield_comma_delimited_options), - ('source_data_id', yield_comma_delimited_options), - ('realization', r'r[0-9]+'), - ('domain', yield_comma_delimited_options), - ('time_frequency', yield_comma_delimited_options), - ('thredds_exclude_variables', yield_comma_delimited_options), - ('variable', yield_comma_delimited_options), - ('dataset_version', r'latest|^v[0-9]*$'), - ('file_period', r'fixed|^\d+-\d+(-clim)?$') + ('work_package', yield_comma_delimited_options), + ('product', yield_comma_delimited_options), + ('source_type', yield_comma_delimited_options), + ('source_data_id', yield_comma_delimited_options), + ('realization', r'r[0-9]+'), + ('domain', yield_comma_delimited_options), + ('time_frequency', yield_comma_delimited_options), + ('thredds_exclude_variables', yield_comma_delimited_options), + ('variable', yield_comma_delimited_options), + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_cc4e_{}_{}_{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable', - 'work_package', - 'product', - 'source_type', - 'source_data_id', - 'realization', - 'domain', - 'time_frequency', - 'file_period' - ), - 'directory_template': 'CC4E/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'work_package', - 'product', - 'source_type', - 'source_data_id', - 'realization', - 'time_frequency', - 'variable', - 'dataset_version' - ), - 'dataset_id_template': 'CC4E.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'work_package', - 'product', - 'source_type', - 'source_data_id', - 'realization', - 'time_frequency', - 'variable', - 'dataset_version' - ) + 'filename': { + 'template': '{}_cc4e_{}_{}_{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable', + 'work_package', + 'product', + 'source_type', + 'source_data_id', + 'realization', + 'domain', + 'time_frequency', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'CC4E/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'work_package', + 'product', + 'source_type', + 'source_data_id', + 'realization', + 'time_frequency', + 'variable', + 'dataset_version' + ) + }, + 'dataset_id': { + 'template': 'CC4E.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'work_package', + 'product', + 'source_type', + 'source_data_id', + 'realization', + 'time_frequency', + 'variable', + 'dataset_version' + ) + } } diff --git a/sh/writers/esgf/map_cmip5.py b/sh/writers/esgf/map_cmip5.py index 151f8a2..025acef 100644 --- a/sh/writers/esgf/map_cmip5.py +++ b/sh/writers/esgf/map_cmip5.py @@ -12,90 +12,93 @@ from utils import yield_comma_delimited_options from utils import yield_pipe_delimited_options - -# TODO process map: institute_map = map(model : institute) -# TODO process map: las_time_delta_map = map(time_frequency : las_time_delta) - # Vocabulary collections extracted from ini file. COLLECTIONS = { - ('cmor_table', yield_comma_delimited_options), - ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), - ('experiment', yield_pipe_delimited_options), - ('model', yield_comma_delimited_options), - ('institute', lambda: yield_institute), - ('las_time_delta', lambda: yield_las_time_delta), - ('time_frequency', yield_comma_delimited_options), - ('product', yield_comma_delimited_options), - ('realm', yield_comma_delimited_options), - ('thredds_exclude_variables', yield_comma_delimited_options), - ('variable', yield_comma_delimited_options), - ('dataset_version', r'latest|^v[0-9]*$'), - ('file_period', r'fixed|^\d+-\d+(-clim)?$') + ('cmor_table', yield_comma_delimited_options), + ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), + ('experiment', yield_pipe_delimited_options), + ('model', yield_comma_delimited_options), + ('institute', lambda: yield_institute), + ('las_time_delta', lambda: yield_las_time_delta), + ('time_frequency', yield_comma_delimited_options), + ('product', yield_comma_delimited_options), + ('realm', yield_comma_delimited_options), + ('thredds_exclude_variables', yield_comma_delimited_options), + ('variable', yield_comma_delimited_options), + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Arbitrary data associated with a collection. COLLECTION_DATA = { - 'experiment': { - 'cim_document_type': 'cim.1.activity.NumericalExperiment', - 'cim_document_type_alternative_name': 'experiment' - }, - 'model': { - 'cim_document_type': 'cim.1.software.ModelComponent', - 'cim_document_type_alternative_name': 'model' - } + 'experiment': { + 'cim_document_type': 'cim.1.activity.NumericalExperiment', + 'cim_document_type_alternative_name': 'experiment' + }, + 'model': { + 'cim_document_type': 'cim.1.software.ModelComponent', + 'cim_document_type_alternative_name': 'model' + } } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable', - 'cmor_table', - 'model', - 'experiment', - 'ensemble', - 'file_period' - ), - 'directory_template': 'CMIP5/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'dataset_version', - 'variable' - ), - 'dataset_id_template': 'cmip5.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'CMIP5/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version', + 'variable' + ), + }, + 'dataset_id': { + 'template': 'cmip5.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version' + ) + } } -def yield_institute(ctx): - """Yields institute information to be converted to pyessv terms. +def yield_las_time_delta(ctx): + """Yields las time delta information to be converted to pyessv terms. - """ - for model, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): - src_namespace = 'wcrp:cmip5:model:{}'.format(model.lower().replace('_','-')) - yield (src_namespace, institute) + """ + for time_frequency, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): + src_namespace = 'wcrp:cmip5:time-frequency:{}'.format(time_frequency.lower().replace('_', '-')) + yield src_namespace, las_time_delta -def yield_las_time_delta(ctx): - """Yields las time delta information to be converted to pyessv terms. +def yield_institute(ctx): + """Yields institute information to be converted to pyessv terms. - """ - for _, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): - yield las_time_delta + """ + for model, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): + src_namespace = 'wcrp:cmip5:model:{}'.format(model.lower().replace('_', '-')) + yield src_namespace, institute diff --git a/sh/writers/esgf/map_cmip6.py b/sh/writers/esgf/map_cmip6.py index 31f749c..b29df99 100644 --- a/sh/writers/esgf/map_cmip6.py +++ b/sh/writers/esgf/map_cmip6.py @@ -11,58 +11,71 @@ """ from utils import yield_comma_delimited_options - -# TODO process maps: institute_map, las_time_delta_map, model_cohort_map -# TODO process map: las_time_delta_map = las_time_delta_map = map(frequency : las_time_delta) - # Vocabulary collections extracted from ini file. COLLECTIONS = { - ('las_time_delta', lambda: yield_las_time_delta), - ('thredds_exclude_variables', yield_comma_delimited_options), + ('las_time_delta', lambda: yield_las_time_delta), + ('model_cohort', lambda: yield_model_cohort), + ('thredds_exclude_variables', yield_comma_delimited_options), } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable_id', - 'table_id', - 'source_id', - 'experiment_id', - 'member_id', - 'grid_label' - 'file_period' - ), - 'directory_template': 'CMIP6/{}/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'activity_id', - 'institution_id', - 'source_id', - 'experiment_id', - 'member_id', - 'table_id', - 'variable_id', - 'grid_label', - 'dataset_version' - ), - 'dataset_id_template': 'CMIP6.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'activity_id', - 'institution_id', - 'source_id', - 'experiment_id', - 'member_id', - 'table_id', - 'variable_id', - 'grid_label', - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable_id', + 'table_id', + 'source_id', + 'experiment_id', + 'member_id', + 'grid_label' + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'CMIP6/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'activity_id', + 'institution_id', + 'source_id', + 'experiment_id', + 'member_id', + 'table_id', + 'variable_id', + 'grid_label', + 'dataset_version' + ) + }, + 'dataset_id': { + 'template': 'CMIP6.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'activity_id', + 'institution_id', + 'source_id', + 'experiment_id', + 'member_id', + 'table_id', + 'variable_id', + 'grid_label', + 'dataset_version' + ) + } } +def yield_model_cohort(ctx): + """Yields model cohort information to be converted to pyessv terms. + + """ + for source_id, model_cohort in ctx.ini_section.get_option('model_cohort_map', '\n', '|'): + src_namespace = 'wcrp:cmip6:source-id:{}'.format(source_id.lower().replace('_', '-')) + yield src_namespace, model_cohort + + def yield_las_time_delta(ctx): - """Yields las time delta information to be converted to pyessv terms. + """Yields las time delta information to be converted to pyessv terms. - """ - for _, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): - yield las_time_delta + """ + for frequency, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): + src_namespace = 'wcrp:cmip6:frequency:{}'.format(frequency.lower().replace('_', '-')) + yield src_namespace, las_time_delta diff --git a/sh/writers/esgf/map_cordex.py b/sh/writers/esgf/map_cordex.py index c990906..e240fac 100644 --- a/sh/writers/esgf/map_cordex.py +++ b/sh/writers/esgf/map_cordex.py @@ -13,92 +13,97 @@ from utils import yield_pipe_delimited_options -# TODO process map: las_time_delta_map = map(time_frequency : las_time_delta) -# TODO process map: rcm_name_map = map(project, rcm_model : rcm_name) - # Vocabulary collections extracted from ini file. COLLECTIONS = { - ('domain', lambda: yield_domain), - ('driving_model', yield_comma_delimited_options), - ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), - ('experiment', yield_pipe_delimited_options), - ('institute', yield_comma_delimited_options), - ('las_time_delta', lambda: yield_las_time_delta), - ('product', yield_comma_delimited_options), - ('rcm_model', yield_comma_delimited_options), - ('rcm_name', lambda: yield_rcm_name), - ('rcm_version', yield_comma_delimited_options), - ('thredds_exclude_variables', yield_comma_delimited_options), - ('time_frequency', yield_comma_delimited_options), - ('variable', yield_comma_delimited_options), - ('dataset_version', r'latest|^v[0-9]*$'), - ('file_period', r'fixed|^\d+-\d+(-clim)?$') + ('domain', lambda: yield_domain), + ('driving_model', yield_comma_delimited_options), + ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), + ('experiment', yield_pipe_delimited_options), + ('institute', yield_comma_delimited_options), + ('las_time_delta', lambda: yield_las_time_delta), + ('product', yield_comma_delimited_options), + ('rcm_model', yield_comma_delimited_options), + ('rcm_name', lambda: yield_rcm_name), + ('rcm_version', yield_comma_delimited_options), + ('thredds_exclude_variables', yield_comma_delimited_options), + ('time_frequency', yield_comma_delimited_options), + ('variable', yield_comma_delimited_options), + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable', - 'domain', - 'driving_model', - 'experiment', - 'ensemble', - 'rcm_model', - 'rcm_version', - 'time_frequency', - 'file_period' - ), - 'directory_template': 'CORDEX/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'product', - 'domain', - 'institute', - 'driving_model', - 'experiment', - 'ensemble', - 'rcm_model', - 'rcm_version', - 'time_frequency', - 'variable', - 'dataset_version' - ), - 'dataset_id_template': 'cordex.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'product', - 'domain', - 'institute', - 'driving_model', - 'experiment', - 'ensemble', - 'rcm_name', - 'rcm_version', - 'time_frequency', - 'variable', - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable', + 'domain', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_model', + 'rcm_version', + 'time_frequency', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'CORDEX/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'product', + 'domain', + 'institute', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_model', + 'rcm_version', + 'time_frequency', + 'variable', + 'dataset_version' + ) + }, + 'dataset_id': { + 'template': 'cordex.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'product', + 'domain', + 'institute', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_name', + 'rcm_version', + 'time_frequency', + 'variable', + 'dataset_version' + ) + } } def yield_domain(ctx): - """Yields domain information to be converted to pyessv terms. + """Yields domain information to be converted to pyessv terms. - """ - for domain_name, domain_description in ctx.ini_section.get_option('domain_description_map', '\n', '|'): - yield domain_name, domain_name, domain_description + """ + for domain_name, domain_description in ctx.ini_section.get_option('domain_description_map', '\n', '|'): + yield domain_name, domain_name, domain_description def yield_las_time_delta(ctx): - """Yields las time delta information to be converted to pyessv terms. + """Yields las time delta information to be converted to pyessv terms. - """ - for _, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): - yield las_time_delta + """ + for time_frequency, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): + src_namespace = 'wcrp:cordex:time-frequency:{}'.format(time_frequency.lower().replace('_', '-')) + yield src_namespace, las_time_delta def yield_rcm_name(ctx): - """Yields rcm name information to be converted to pyessv terms. + """Yields rcm name information to be converted to pyessv terms. - """ - for _, rcm_name in ctx.ini_section.get_option('rcm_name_map', '\n', '|'): - yield rcm_name + """ + for rcm_model, rcm_name in ctx.ini_section.get_option('rcm_name_map', '\n', '|'): + src_namespace = 'wcrp:cordex:rcm_model:{}'.format(rcm_model.lower().replace('_', '-')) + yield src_namespace, rcm_name diff --git a/sh/writers/esgf/map_cordex_adjust.py b/sh/writers/esgf/map_cordex_adjust.py index c008550..a2f1ab3 100644 --- a/sh/writers/esgf/map_cordex_adjust.py +++ b/sh/writers/esgf/map_cordex_adjust.py @@ -13,92 +13,97 @@ from utils import yield_pipe_delimited_options -# TODO process map: las_time_delta_map = map(time_frequency : las_time_delta) -# TODO process map: rcm_name_map = map(project, rcm_model : rcm_name) - # Vocabulary collections extracted from ini file. COLLECTIONS = { - ('domain', lambda: yield_domain), - ('driving_model', yield_comma_delimited_options), - ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), - ('experiment', yield_pipe_delimited_options), - ('institute', yield_comma_delimited_options), - ('las_time_delta', lambda: yield_las_time_delta), - ('product', yield_comma_delimited_options), - ('rcm_model', yield_comma_delimited_options), - ('rcm_name', lambda: yield_rcm_name), - ('bias_adjustment', yield_comma_delimited_options), - ('thredds_exclude_variables', yield_comma_delimited_options), - ('time_frequency', yield_comma_delimited_options), - ('variable', yield_comma_delimited_options), - ('dataset_version', r'latest|v^[0-9]*$'), - ('file_period', r'fixed|^\d+-\d+(-clim)?$') + ('domain', lambda: yield_domain), + ('driving_model', yield_comma_delimited_options), + ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), + ('experiment', yield_pipe_delimited_options), + ('institute', yield_comma_delimited_options), + ('las_time_delta', lambda: yield_las_time_delta), + ('product', yield_comma_delimited_options), + ('rcm_model', yield_comma_delimited_options), + ('rcm_name', lambda: yield_rcm_name), + ('bias_adjustment', yield_comma_delimited_options), + ('thredds_exclude_variables', yield_comma_delimited_options), + ('time_frequency', yield_comma_delimited_options), + ('variable', yield_comma_delimited_options), + ('dataset_version', r'latest|v^[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable', - 'domain', - 'driving_model', - 'experiment', - 'ensemble', - 'rcm_model', - 'bias_adjustment', - 'time_frequency', - 'file_period' - ), - 'directory_template': 'CORDEX-Adjust/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'product', - 'domain', - 'institute', - 'driving_model', - 'experiment', - 'ensemble', - 'rcm_model', - 'bias_adjustment', - 'time_frequency', - 'variable', - 'dataset_version' - ), - 'dataset_id_template': 'cordex-adjust.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'product', - 'domain', - 'institute', - 'driving_model', - 'experiment', - 'ensemble', - 'rcm_name', - 'bias_adjustment', - 'time_frequency', - 'variable', - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable', + 'domain', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_model', + 'bias_adjustment', + 'time_frequency', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'CORDEX-Adjust/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'product', + 'domain', + 'institute', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_model', + 'bias_adjustment', + 'time_frequency', + 'variable', + 'dataset_version' + ) + }, + 'dataset_id': { + 'template': 'cordex-adjust.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'product', + 'domain', + 'institute', + 'driving_model', + 'experiment', + 'ensemble', + 'rcm_name', + 'bias_adjustment', + 'time_frequency', + 'variable', + 'dataset_version' + ) + } } def yield_domain(ctx): - """Yields domain information to be converted to pyessv terms. + """Yields domain information to be converted to pyessv terms. - """ - for domain_name, domain_description in ctx.ini_section.get_option('domain_description_map', '\n', '|'): - yield domain_name, domain_name, domain_description + """ + for domain_name, domain_description in ctx.ini_section.get_option('domain_description_map', '\n', '|'): + yield domain_name, domain_name, domain_description def yield_las_time_delta(ctx): - """Yields las time delta information to be converted to pyessv terms. + """Yields las time delta information to be converted to pyessv terms. - """ - for _, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): - yield las_time_delta + """ + for time_frequency, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): + src_namespace = 'wcrp:cordex-adjust:time-frequency:{}'.format(time_frequency.lower().replace('_', '-')) + yield src_namespace, las_time_delta def yield_rcm_name(ctx): - """Yields rcm name information to be converted to pyessv terms. + """Yields rcm name information to be converted to pyessv terms. - """ - for _, rcm_name in ctx.ini_section.get_option('rcm_name_map', '\n', '|'): - yield rcm_name + """ + for rcm_model, rcm_name in ctx.ini_section.get_option('rcm_name_map', '\n', '|'): + src_namespace = 'wcrp:cordex-adjust:rcm_model:{}'.format(rcm_model.lower().replace('_', '-')) + yield src_namespace, rcm_name diff --git a/sh/writers/esgf/map_euclipse.py b/sh/writers/esgf/map_euclipse.py index edf2f47..912d20e 100644 --- a/sh/writers/esgf/map_euclipse.py +++ b/sh/writers/esgf/map_euclipse.py @@ -13,9 +13,6 @@ from utils import yield_pipe_delimited_options -# TODO process map: institute_map = map(model : institute) -# TODO process map: las_time_delta_map = map(time_frequency : las_time_delta) - # Vocabulary collections extracted from ini file. COLLECTIONS = { ('cmor_table', yield_comma_delimited_options), @@ -36,54 +33,62 @@ # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable', - 'cmor_table', - 'model', - 'experiment', - 'ensemble', - 'file_period' - ), - 'directory_template': 'EUCLIPSE/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'dataset_version', - 'variable' - ), - 'dataset_id_template': 'euclipse.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'EUCLIPSE/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version', + 'variable' + ), + }, + 'dataset_id': { + 'template': 'euclipse.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version' + ) + } } -def yield_institute(ctx): - """Yields institute information to be converted to pyessv terms. +def yield_las_time_delta(ctx): + """Yields las time delta information to be converted to pyessv terms. - """ - for _, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): - yield institute + """ + for time_frequency, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): + src_namespace = 'wcrp:euclipse:time-frequency:{}'.format(time_frequency.lower().replace('_', '-')) + yield src_namespace, las_time_delta -def yield_las_time_delta(ctx): - """Yields las time delta information to be converted to pyessv terms. +def yield_institute(ctx): + """Yields institute information to be converted to pyessv terms. - """ - for _, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): - yield las_time_delta + """ + for model, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): + src_namespace = 'wcrp:euclipse:model:{}'.format(model.lower().replace('_', '-')) + yield src_namespace, institute diff --git a/sh/writers/esgf/map_geomip.py b/sh/writers/esgf/map_geomip.py index 920ec1d..644cc52 100644 --- a/sh/writers/esgf/map_geomip.py +++ b/sh/writers/esgf/map_geomip.py @@ -13,9 +13,6 @@ from utils import yield_pipe_delimited_options -# TODO process map: institute_map = map(model : institute) -# TODO process map: las_time_delta_map = map(time_frequency : las_time_delta) - # Vocabulary collections extracted from ini file. COLLECTIONS = { ('cmor_table', yield_comma_delimited_options), @@ -35,54 +32,63 @@ # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable', - 'cmor_table', - 'model', - 'experiment', - 'ensemble', - 'file_period' - ), - 'directory_template': 'GeoMIP/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'dataset_version', - 'variable' - ), - 'dataset_id_template': 'geomip.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'GeoMIP/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version', + 'variable' + ), + }, + 'dataset_id': { + 'template': 'geomip.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version' + ) + } } -def yield_institute(ctx): - """Yields institute information to be converted to pyessv terms. +def yield_las_time_delta(ctx): + """Yields las time delta information to be converted to pyessv terms. - """ - for _, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): - yield institute + """ + for time_frequency, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): + src_namespace = 'wcrp:geomip:time-frequency:{}'.format(time_frequency.lower().replace('_', '-')) + yield src_namespace, las_time_delta -def yield_las_time_delta(ctx): - """Yields las time delta information to be converted to pyessv terms. +def yield_institute(ctx): + """Yields institute information to be converted to pyessv terms. + + """ + for model, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): + src_namespace = 'wcrp:geomip:model:{}'.format(model.lower().replace('_', '-')) + yield src_namespace, institute - """ - for _, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): - yield las_time_delta diff --git a/sh/writers/esgf/map_input4mips.py b/sh/writers/esgf/map_input4mips.py index 8272c83..6cb2271 100644 --- a/sh/writers/esgf/map_input4mips.py +++ b/sh/writers/esgf/map_input4mips.py @@ -12,62 +12,68 @@ from utils import get_ini_option from utils import yield_comma_delimited_options - # Vocabulary collections extracted from ini file. COLLECTIONS = { - ('variable_id', lambda: yield_variable_id_options), - ('activity_id', yield_comma_delimited_options), - ('dataset_category', r'^[A-Za-z0-9]*$'), - ('target_mip', yield_comma_delimited_options), - ('source_id', yield_comma_delimited_options), - ('grid_label', yield_comma_delimited_options), - ('institution_id', yield_comma_delimited_options), - ('realm', yield_comma_delimited_options), + ('variable_id', lambda: yield_variable_id_options), + ('activity_id', yield_comma_delimited_options), + ('dataset_category', r'^[A-Za-z0-9]*$'), + ('target_mip', yield_comma_delimited_options), + ('source_id', yield_comma_delimited_options), + ('grid_label', yield_comma_delimited_options), + ('institution_id', yield_comma_delimited_options), + ('realm', yield_comma_delimited_options), ('mip_era', yield_comma_delimited_options), - ('frequency', yield_comma_delimited_options), - ('thredds_exclude_variables', yield_comma_delimited_options), - ('dataset_version', r'latest|^v[0_9]{8}$'), + ('frequency', yield_comma_delimited_options), + ('thredds_exclude_variables', yield_comma_delimited_options), + ('dataset_version', r'latest|^v[0_9]{8}$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable_id', - 'activity_id', - 'dataset_category', - 'target_mip', - 'source_id', - 'grid_label' - 'file_period' - ), - 'directory_template': 'input4MIPs/{}/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'mip_era', - 'target_mip', - 'institution_id', - 'source_id', - 'realm', - 'frequency', - 'variable_id', - 'grid_label', - 'dataset_version' - ), - 'dataset_id_template': 'input4MIPs.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'mip_era', - 'target_mip', - 'institution_id', - 'source_id', - 'realm', - 'frequency', - 'variable_id', - 'grid_label', - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable_id', + 'activity_id', + 'dataset_category', + 'target_mip', + 'source_id', + 'grid_label' + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'input4MIPs/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'mip_era', + 'target_mip', + 'institution_id', + 'source_id', + 'realm', + 'frequency', + 'variable_id', + 'grid_label', + 'dataset_version' + ) + }, + 'dataset_id': { + 'template': 'input4MIPs.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'mip_era', + 'target_mip', + 'institution_id', + 'source_id', + 'realm', + 'frequency', + 'variable_id', + 'grid_label', + 'dataset_version' + ) + } } + def yield_variable_id_options(ctx): # Decode options from ini file. opts = get_ini_option(ctx) diff --git a/sh/writers/esgf/map_isimip_ft.py b/sh/writers/esgf/map_isimip_ft.py index b60cf11..00c5180 100644 --- a/sh/writers/esgf/map_isimip_ft.py +++ b/sh/writers/esgf/map_isimip_ft.py @@ -13,88 +13,99 @@ from utils import yield_pipe_delimited_options -# TODO process map: institute_map = map(model : institute) -# TODO process map: las_time_delta_map = map(time_frequency : las_time_delta) - - # Vocabulary collections extracted from ini file. COLLECTIONS = { - ('product', yield_comma_delimited_options), - ('model', yield_comma_delimited_options), - ('impact_model', yield_comma_delimited_options), - ('experiment', yield_pipe_delimited_options), - ('sector_short', yield_comma_delimited_options), - ('social_forcing', yield_comma_delimited_options), - ('co2_forcing', yield_comma_delimited_options), - ('irrigation_forcing', yield_comma_delimited_options), - ('land_use_short', yield_comma_delimited_options), - ('time_frequency', yield_comma_delimited_options), - ('variable', yield_comma_delimited_options), - ('dataset_version', r'latest|^v[0-9]*$'), - ('thredds_exclude_variables', yield_comma_delimited_options), - ('file_period', r'fixed|^\d+-\d+(-clim)?$') + ('product', yield_comma_delimited_options), + ('model', yield_comma_delimited_options), + ('impact_model', yield_comma_delimited_options), + ('experiment', yield_pipe_delimited_options), + ('sector_short', lambda: yield_sector), + ('social_forcing', yield_comma_delimited_options), + ('institute', lambda: yield_institute), + ('co2_forcing', yield_comma_delimited_options), + ('irrigation_forcing', yield_comma_delimited_options), + ('land_use_short', lambda: yield_land_use), + ('time_frequency', yield_comma_delimited_options), + ('variable', yield_comma_delimited_options), + ('dataset_version', r'latest|^v[0-9]*$'), + ('thredds_exclude_variables', yield_comma_delimited_options), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } - # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'impact_model', - 'model', - 'experiment', - 'social_forcing', - 'co2_forcing', - 'irrigation_forcing', - 'land_use_short', - 'variable' - 'time_frequency', - 'file_period' - ), - 'directory_template': 'ISIMIP-FT/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'product', - 'model', - 'experiment', - 'impact_model', - 'sector_short', - 'social_forcing', - 'co2_forcing', - 'irrigation_forcing', - 'land_use_short', - 'variable' - 'time_frequency', - 'dataset_version' - ), - 'dataset_id_template': 'isimip-ft.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'product', - 'impact_model', - 'sector_short', - 'model', - 'experiment', - 'social_forcing', - 'co2_forcing', - 'irrigation_forcing', - 'time_frequency', - 'land_use_short', - 'variable' - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}_{}_{}_{}_{}_{}', + 'collections': ( + 'impact_model', + 'model', + 'experiment', + 'social_forcing', + 'co2_forcing', + 'irrigation_forcing', + 'land_use_short', + 'variable' + 'time_frequency', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'ISIMIP-FT/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'product', + 'model', + 'experiment', + 'impact_model', + 'sector_short', + 'social_forcing', + 'co2_forcing', + 'irrigation_forcing', + 'land_use_short', + 'variable' + 'time_frequency', + 'dataset_version' + ) + }, + 'dataset_id': { + 'template': 'isimip-ft.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'product', + 'impact_model', + 'sector_short', + 'model', + 'experiment', + 'social_forcing', + 'co2_forcing', + 'irrigation_forcing', + 'time_frequency', + 'land_use_short', + 'variable' + 'dataset_version' + ) + } } def yield_institute(ctx): - """Yields institute information to be converted to pyessv terms. + """Yields institute information to be converted to pyessv terms. + + """ + for model, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): + src_namespace = 'wcrp:isimip-ft:impact-model:{}'.format(model.lower().replace('_', '-')) + yield src_namespace, institute + + +def yield_land_use(ctx): + """Yields land use information to be converted to pyessv terms. - """ - for _, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): - yield institute + """ + for land_use_acronym, land_use in ctx.ini_section.get_option('land_use_map', '\n', '|'): + yield land_use_acronym, land_use_acronym, land_use -def yield_las_time_delta(ctx): - """Yields las time delta information to be converted to pyessv terms. +def yield_sector(ctx): + """Yields sector information to be converted to pyessv terms. - """ - for _, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): - yield las_time_delta + """ + for sector_acronym, sector in ctx.ini_section.get_option('sector_map', '\n', '|'): + yield sector_acronym, sector_acronym, sector diff --git a/sh/writers/esgf/map_lucid.py b/sh/writers/esgf/map_lucid.py index 52cf45c..c2ec6ea 100644 --- a/sh/writers/esgf/map_lucid.py +++ b/sh/writers/esgf/map_lucid.py @@ -13,9 +13,6 @@ from utils import yield_pipe_delimited_options -# TODO process map: institute_map = map(model : institute) -# TODO process map: las_time_delta_map = map(time_frequency : las_time_delta) - # Vocabulary collections extracted from ini file. COLLECTIONS = { ('cmor_table', yield_comma_delimited_options), @@ -35,54 +32,63 @@ # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable', - 'cmor_table', - 'model', - 'experiment', - 'ensemble', - 'file_period' - ), - 'directory_template': 'LUCID/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'dataset_version', - 'variable' - ), - 'dataset_id_template': 'lucid.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'LUCID/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version', + 'variable' + ), + }, + 'dataset_id': { + 'template': 'lucid.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version' + ) + } } -def yield_institute(ctx): - """Yields institute information to be converted to pyessv terms. +def yield_las_time_delta(ctx): + """Yields las time delta information to be converted to pyessv terms. - """ - for _, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): - yield institute + """ + for time_frequency, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): + src_namespace = 'wcrp:lucid:time-frequency:{}'.format(time_frequency.lower().replace('_', '-')) + yield src_namespace, las_time_delta -def yield_las_time_delta(ctx): - """Yields las time delta information to be converted to pyessv terms. +def yield_institute(ctx): + """Yields institute information to be converted to pyessv terms. + + """ + for model, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): + src_namespace = 'wcrp:lucid:model:{}'.format(model.lower().replace('_', '-')) + yield src_namespace, institute - """ - for _, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): - yield las_time_delta diff --git a/sh/writers/esgf/map_obs4mips.py b/sh/writers/esgf/map_obs4mips.py index 8ce733c..808f5d5 100644 --- a/sh/writers/esgf/map_obs4mips.py +++ b/sh/writers/esgf/map_obs4mips.py @@ -68,13 +68,15 @@ def yield_variable(ctx): """Yields institute information to be converted to pyessv terms. """ - for var, _ in ctx.ini_section.get_option('variable_map', '\n', '|'): - yield var + for var, variable in ctx.ini_section.get_option('variable_map', '\n', '|'): + src_namespace = 'wcrp:obs4mips:var:{}'.format(var.lower().replace('_','-')) + yield src_namespace, variable def yield_las_time_delta(ctx): """Yields las time delta information to be converted to pyessv terms. """ - for _, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): - yield las_time_delta + for time_frequency, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): + src_namespace = 'wcrp:obs4mips:time-frequency:{}'.format(time_frequency.lower().replace('_', '-')) + yield src_namespace, las_time_delta diff --git a/sh/writers/esgf/map_pmip3.py b/sh/writers/esgf/map_pmip3.py index 1664346..dd6f125 100644 --- a/sh/writers/esgf/map_pmip3.py +++ b/sh/writers/esgf/map_pmip3.py @@ -13,9 +13,6 @@ from utils import yield_pipe_delimited_options -# TODO process map: institute_map = map(model : institute) -# TODO process map: las_time_delta_map = map(time_frequency : las_time_delta) - # Vocabulary collections extracted from ini file. COLLECTIONS = { ('cmor_table', yield_comma_delimited_options), @@ -35,54 +32,62 @@ # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable', - 'cmor_table', - 'model', - 'experiment', - 'ensemble', - 'file_period' - ), - 'directory_template': 'PMIP3/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'dataset_version', - 'variable' - ), - 'dataset_id_template': 'pmip3.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'PMIP3/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version', + 'variable' + ), + }, + 'dataset_id': { + 'template': 'pmip3.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version' + ) + } } -def yield_institute(ctx): - """Yields institute information to be converted to pyessv terms. +def yield_las_time_delta(ctx): + """Yields las time delta information to be converted to pyessv terms. - """ - for _, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): - yield institute + """ + for time_frequency, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): + src_namespace = 'wcrp:pmip3:time-frequency:{}'.format(time_frequency.lower().replace('_', '-')) + yield src_namespace, las_time_delta -def yield_las_time_delta(ctx): - """Yields las time delta information to be converted to pyessv terms. +def yield_institute(ctx): + """Yields institute information to be converted to pyessv terms. - """ - for _, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): - yield las_time_delta + """ + for model, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): + src_namespace = 'wcrp:pmip3:model:{}'.format(model.lower().replace('_', '-')) + yield src_namespace, institute diff --git a/sh/writers/esgf/map_primavera.py b/sh/writers/esgf/map_primavera.py index b1668c5..4a7b509 100644 --- a/sh/writers/esgf/map_primavera.py +++ b/sh/writers/esgf/map_primavera.py @@ -12,66 +12,70 @@ from utils import yield_comma_delimited_options from utils import yield_pipe_delimited_options - -# TODO process map: institute_map = map(model : institute) - # Vocabulary collections extracted from ini file. COLLECTIONS = { - ('activity', yield_comma_delimited_options), - ('institute', lambda: yield_institute), - ('model', yield_comma_delimited_options), - ('experiment', yield_pipe_delimited_options), - ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+f[0-9]+'), - ('cmor_table', yield_comma_delimited_options), - ('variable', r'^[A-Za-z0-9]*$'), - ('grid_label', yield_comma_delimited_options), - ('thredds_exclude_variables', yield_comma_delimited_options), - ('dataset_version', r'latest|^v[0-9]*$'), - ('file_period', r'fixed|^\d+-\d+(-clim)?$') + ('activity', yield_comma_delimited_options), + ('institute', lambda: yield_institute), + ('model', yield_comma_delimited_options), + ('experiment', yield_pipe_delimited_options), + ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+f[0-9]+'), + ('cmor_table', yield_comma_delimited_options), + ('variable', r'^[A-Za-z0-9]*$'), + ('grid_label', yield_comma_delimited_options), + ('thredds_exclude_variables', yield_comma_delimited_options), + ('dataset_version', r'latest|^v[0-9]*$'), + ('file_period', r'fixed|^\d+-\d+(-clim)?$') } # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable', - 'cmor_table', - 'model', - 'experiment', - 'ensemble', - 'grid_label', - 'file_period' - ), - 'directory_template': 'PRIMAVERA/{}/{}=/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'activity', - 'institute', - 'model', - 'experiment', - 'ensemble', - 'cmor_table', - 'variable', - 'grid_version', - 'dataset_version' - ), - 'dataset_id_template': 'PRIMAVERA.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'activity', - 'institute', - 'model', - 'experiment', - 'ensemble', - 'cmor_table', - 'variable', - 'grid_version', - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'grid_label', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'PRIMAVERA/{}/{}=/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'activity', + 'institute', + 'model', + 'experiment', + 'ensemble', + 'cmor_table', + 'variable', + 'grid_version', + 'dataset_version' + ) + }, + 'dataset_id': { + 'template': 'PRIMAVERA.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'activity', + 'institute', + 'model', + 'experiment', + 'ensemble', + 'cmor_table', + 'variable', + 'grid_version', + 'dataset_version' + ) + } } def yield_institute(ctx): - """Yields institute information to be converted to pyessv terms. + """Yields institute information to be converted to pyessv terms. - """ - for _, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): - yield institute + """ + for model, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): + src_namespace = 'wcrp:primavera:model:{}'.format(model.lower().replace('_', '-')) + yield src_namespace, institute diff --git a/sh/writers/esgf/map_tamip.py b/sh/writers/esgf/map_tamip.py index e576438..a8ff928 100644 --- a/sh/writers/esgf/map_tamip.py +++ b/sh/writers/esgf/map_tamip.py @@ -13,9 +13,6 @@ from utils import yield_pipe_delimited_options -# TODO process map: institute_map = map(model : institute) -# TODO process map: las_time_delta_map = map(time_frequency : las_time_delta) - # Vocabulary collections extracted from ini file. COLLECTIONS = { ('cmor_table', yield_comma_delimited_options), @@ -35,54 +32,63 @@ # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable', - 'cmor_table', - 'model', - 'experiment', - 'ensemble', - 'file_period' - ), - 'directory_template': 'TAMIP/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'dataset_version', - 'variable' - ), - 'dataset_id_template': 'tamip.{}.{}.{}.{}.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'product', - 'institute', - 'model', - 'experiment', - 'time_frequency', - 'realm', - 'cmor_table', - 'ensemble', - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}_{}', + 'collections': ( + 'variable', + 'cmor_table', + 'model', + 'experiment', + 'ensemble', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'TAMIP/{}/{}/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version', + 'variable' + ), + }, + 'dataset_id': { + 'template': 'tamip.{}.{}.{}.{}.{}.{}.{}.{}.{}', + 'collections': ( + 'product', + 'institute', + 'model', + 'experiment', + 'time_frequency', + 'realm', + 'cmor_table', + 'ensemble', + 'dataset_version' + ) + } } -def yield_institute(ctx): - """Yields institute information to be converted to pyessv terms. +def yield_las_time_delta(ctx): + """Yields las time delta information to be converted to pyessv terms. - """ - for _, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): - yield institute + """ + for time_frequency, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): + src_namespace = 'wcrp:tamip:time-frequency:{}'.format(time_frequency.lower().replace('_', '-')) + yield src_namespace, las_time_delta -def yield_las_time_delta(ctx): - """Yields las time delta information to be converted to pyessv terms. +def yield_institute(ctx): + """Yields institute information to be converted to pyessv terms. + + """ + for model, institute in ctx.ini_section.get_option('institute_map', '\n', '|'): + src_namespace = 'wcrp:tamip:model:{}'.format(model.lower().replace('_', '-')) + yield src_namespace, institute - """ - for _, las_time_delta in ctx.ini_section.get_option('las_time_delta_map', '\n', '|'): - yield las_time_delta From 235d111617372069e39f7c63a3a8bad107cbd161 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Thu, 18 Apr 2019 11:18:53 +0200 Subject: [PATCH 12/14] Find appropriate term among associations --- pyessv/_builder_template.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyessv/_builder_template.py b/pyessv/_builder_template.py index 917dc36..474ac68 100644 --- a/pyessv/_builder_template.py +++ b/pyessv/_builder_template.py @@ -70,6 +70,12 @@ def build(self, terms, att='label', alt_name=0): if term.collection == collection: break + # Append term from associations. + if not term: + for term in [association for association in t.associations for t in terms]: + if term.collection == collection: + break + # Verify collection is found among terms. if not term: raise TemplateValueError('Collection not found among terms :: {}'.format(collection)) From fecc2297d67a79d8b375ad1c753fe4c3b33874f8 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 7 Aug 2019 10:58:09 +0200 Subject: [PATCH 13/14] bugfixes for python 3 --- pyessv/__init__.py | 2 +- pyessv/_builder_template.py | 2 ++ pyessv/_builders/dataset_id.py | 10 ++++++---- pyessv/_codecs/json_codec/encoder.py | 2 +- pyessv/_parsers/dataset_id.py | 10 ++++++---- pyessv/_parsers/directory.py | 10 ++++++---- pyessv/_parsers/filename.py | 10 ++++++---- 7 files changed, 28 insertions(+), 18 deletions(-) diff --git a/pyessv/__init__.py b/pyessv/__init__.py index 9addbdd..42bad6b 100644 --- a/pyessv/__init__.py +++ b/pyessv/__init__.py @@ -11,7 +11,7 @@ """ __title__ = 'pyessv' -__version__ = '0.7.0.0' +__version__ = '0.7.0.1' __author__ = 'ES-DOC' __license__ = 'GPL' __copyright__ = 'Copyright 2017 ES-DOC' diff --git a/pyessv/_builder_template.py b/pyessv/_builder_template.py index 474ac68..4f30080 100644 --- a/pyessv/_builder_template.py +++ b/pyessv/_builder_template.py @@ -14,6 +14,8 @@ from pyessv._model import Collection, Term from pyessv._exceptions import TemplateParsingError, TemplateValueError from pyessv._constants import BUILDER_FIELDS +from pyessv._utils.compat import basestring, str + class TemplateBuilder(object): """A vocabulary template builder. diff --git a/pyessv/_builders/dataset_id.py b/pyessv/_builders/dataset_id.py index 57a45d4..2b632d3 100644 --- a/pyessv/_builders/dataset_id.py +++ b/pyessv/_builders/dataset_id.py @@ -55,15 +55,17 @@ def build_dataset_identifier(project, terms): assert project in [scope.name for scope in scopes], 'Unsupported project' scope = [scope for scope in scopes if scope.name == project][0] + assert 'dataset_id' in scope.data.keys(), 'Dataset ID parser not found' + assert 'template' in scope.data['dataset_id'].keys(), 'Dataset ID parser template not found' + assert 'collections' in scope.data['dataset_id'].keys(), 'Dataset ID parser template collections not found' + # Get template from data scope. - assert 'dataset_id_template' in scope.data.keys(), 'Dataset ID template not found' - _TEMPLATE = scope.data['dataset_id_template'] + _TEMPLATE = scope.data['dataset_id']['template'] assert isinstance(_TEMPLATE, basestring), 'Invalid template' # Get template collections from data scope. - assert 'dataset_id_collections' in scope.data.keys(), 'Template collections not found' _COLLECTIONS = list() - for name in scope.data['dataset_id_collections']: + for name in scope.data['dataset_id']['collections']: _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) assert _COLLECTIONS, 'Invalid collections' diff --git a/pyessv/_codecs/json_codec/encoder.py b/pyessv/_codecs/json_codec/encoder.py index fd6a51d..cf83a0d 100644 --- a/pyessv/_codecs/json_codec/encoder.py +++ b/pyessv/_codecs/json_codec/encoder.py @@ -20,7 +20,7 @@ from pyessv._utils import convert from pyessv._utils.compat import json from pyessv._utils.compat import numeric_types -from pyessv._utils.compat import str +from pyessv._utils.compat import str, basestring diff --git a/pyessv/_parsers/dataset_id.py b/pyessv/_parsers/dataset_id.py index 1c73555..ccdd0ea 100644 --- a/pyessv/_parsers/dataset_id.py +++ b/pyessv/_parsers/dataset_id.py @@ -75,15 +75,17 @@ def parse_dataset_identifier(project, identifier): assert project in [scope.name for scope in scopes], 'Unsupported project' scope = [scope for scope in scopes if scope.name == project][0] + assert 'dataset_id' in scope.data.keys(), 'Dataset ID parser not found' + assert 'template' in scope.data['dataset_id'].keys(), 'Dataset ID parser template not found' + assert 'collections' in scope.data['dataset_id'].keys(), 'Dataset ID parser template collections not found' + # Get template from data scope. - assert 'dataset_id_template' in scope.data.keys(), 'Dataset ID template not found' - _TEMPLATE = scope.data['dataset_id_template'] + _TEMPLATE = scope.data['dataset_id']['template'] assert isinstance(_TEMPLATE, basestring), 'Invalid template' # Get template collections from data scope. - assert 'dataset_id_collections' in scope.data.keys(), 'Template collections not found' _COLLECTIONS = list() - for name in scope.data['dataset_id_collections']: + for name in scope.data['dataset_id']['collections']: _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) assert _COLLECTIONS, 'Invalid collections' diff --git a/pyessv/_parsers/directory.py b/pyessv/_parsers/directory.py index 74d3138..30c27c5 100644 --- a/pyessv/_parsers/directory.py +++ b/pyessv/_parsers/directory.py @@ -84,15 +84,17 @@ def parse_directory(project, directory): assert project in [scope.name for scope in scopes], 'Unsupported project' scope = [scope for scope in scopes if scope.name == project][0] + assert 'directory_structure' in scope.data.keys(), 'Directory parser not found' + assert 'template' in scope.data['directory_structure'].keys(), 'Directory parser template not found' + assert 'collections' in scope.data['directory_structure'].keys(), 'Directory parser template collections not found' + # Get template from data scope. - assert 'directory_template' in scope.data.keys(), 'Directory template not found' - _TEMPLATE = scope.data['directory_template'] + _TEMPLATE = scope.data['directory_structure']['template'] assert isinstance(_TEMPLATE, basestring), 'Invalid template' # Get template collections from data scope. - assert 'directory_collections' in scope.data.keys(), 'Template collections not found' _COLLECTIONS = list() - for name in scope.data['directory_collections']: + for name in scope.data['directory_structure']['collections']: _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) assert _COLLECTIONS, 'Invalid collections' diff --git a/pyessv/_parsers/filename.py b/pyessv/_parsers/filename.py index ba0a510..91f4ee0 100644 --- a/pyessv/_parsers/filename.py +++ b/pyessv/_parsers/filename.py @@ -80,15 +80,17 @@ def parse_filename(project, filename): assert project in [scope.name for scope in scopes], 'Unsupported project' scope = [scope for scope in scopes if scope.name == project][0] + assert 'filename' in scope.data.keys(), 'Filename parser not found' + assert 'template' in scope.data['filename'].keys(), 'Filename parser template not found' + assert 'collections' in scope.data['filename'].keys(), 'Filename parser template collections not found' + # Get template from data scope. - assert 'filename_template' in scope.data.keys(), 'Filename template not found' - _TEMPLATE = scope.data['filename_template'] + _TEMPLATE = scope.data['filename']['template'] assert isinstance(_TEMPLATE, basestring), 'Invalid template' # Get template collections from data scope. - assert 'filename_collections' in scope.data.keys(), 'Filename collections not found' _COLLECTIONS = list() - for name in scope.data['filename_collections']: + for name in scope.data['filename']['collections']: _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) assert _COLLECTIONS, 'Invalid collections' From be33d56e3708689a28c499dcf701a9c84eacbf58 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Mon, 26 Aug 2019 12:07:05 +0200 Subject: [PATCH 14/14] update aside of esgprep3.0 release --- my.pkl | Bin 0 -> 592114 bytes pyessv/__init__.py | 2 + pyessv/_builder_template.py | 5 +- pyessv/_builders/__init__.py | 2 + pyessv/_builders/dataset_id.py | 8 +- pyessv/_builders/directory.py | 77 +++++++++++++++++ pyessv/_builders/filename.py | 77 +++++++++++++++++ pyessv/_initializer.py | 119 ++++++++++++--------------- pyessv/_io_manager.py | 17 +++- pyessv/_model/node.py | 13 ++- pyessv/_parser.py | 11 ++- pyessv/_parsers/filename.py | 1 + sh/writers/esgf/map.py | 8 +- sh/writers/esgf/map_c3s_cmip5.py | 6 +- sh/writers/esgf/map_c3s_cordex.py | 4 +- sh/writers/esgf/map_cc4e.py | 4 +- sh/writers/esgf/map_cmip5.py | 6 +- sh/writers/esgf/map_cmip6.py | 4 +- sh/writers/esgf/map_cordex.py | 8 +- sh/writers/esgf/map_cordex_adjust.py | 6 +- sh/writers/esgf/map_euclipse.py | 12 +-- sh/writers/esgf/map_geomip.py | 8 +- sh/writers/esgf/map_input4mips.py | 4 +- sh/writers/esgf/map_isimip_ft.py | 4 +- sh/writers/esgf/map_lucid.py | 8 +- sh/writers/esgf/map_obs4mips.py | 66 ++++++++------- sh/writers/esgf/map_pmip3.py | 8 +- sh/writers/esgf/map_primavera.py | 6 +- sh/writers/esgf/map_tamip.py | 8 +- sh/writers/wcrp/cmip6/write.py | 2 +- 30 files changed, 338 insertions(+), 166 deletions(-) create mode 100644 my.pkl create mode 100644 pyessv/_builders/directory.py create mode 100644 pyessv/_builders/filename.py diff --git a/my.pkl b/my.pkl new file mode 100644 index 0000000000000000000000000000000000000000..d054621499987e45cb194e68d4b8ce7dea1cac43 GIT binary patch literal 592114 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 @@ -16,10 +16,6 @@ from pyessv._factory import create_template_builder from pyessv._utils.compat import basestring -# Test identifier (for reference purpose only). -_TEST_CMIP5_IDENTIFIER = 'cmip5.output2.IPSL.IPSL-CM5A-LR.historicalMisc.mon.ocean.Omon.r2i1p1.v20150504' -_TEST_CORDEX_IDENTIFIER = 'cordex.output.AFR-44.MOHC.MOHC-HadGEM2-ES.rcp60.r12i1p1.HadGEM3-RA.v1.mon.areacella#20190804' - # Instantiated template _TEMPLATE = None diff --git a/pyessv/_builders/directory.py b/pyessv/_builders/directory.py new file mode 100644 index 0000000..71f23b9 --- /dev/null +++ b/pyessv/_builders/directory.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +""" +.. module:: pyessv._builders.dataset_id.py + :copyright: Copyright "December 01, 2016", IPSL + :license: GPL/CeCIL + :platform: Unix, Windows + :synopsis: Encapsulates building of an ESGF dataset identifier. + +.. moduleauthor:: Mark Conway-Greenslade + +""" +from pyessv._model.term import Term +from pyessv import all_scopes +from pyessv._constants import PARSING_STRICTNESS_1 +from pyessv._factory import create_template_builder +from pyessv._utils.compat import basestring + +# Instantiated template +_TEMPLATE = None + +# Instantiated template collections +_COLLECTIONS = None + +# Instantiated project. +_PROJECT = None + +# Instantiated builder. +_BUILDER = None + + +def build_directory(project, terms): + """Builds a directory. + + :param str project: Project code. + :param set terms: Directory terms. + + :returns: Directory string. + :rtype: str + + """ + assert isinstance(project, basestring), 'Invalid project' + assert isinstance(terms, set), 'Invalid terms' + + global _PROJECT, _BUILDER, _TEMPLATE, _COLLECTIONS + + if _PROJECT != project: + + # Get scope corresponding to the project code. + scopes = all_scopes() + assert project in [scope.name for scope in scopes], 'Unsupported project' + scope = [scope for scope in scopes if scope.name == project][0] + + assert 'directory_structure' in scope.data.keys(), 'Directory parser not found' + assert 'template' in scope.data['directory_structure'].keys(), 'Directory parser template not found' + assert 'collections' in scope.data['directory_structure'].keys(), 'Directory parser template collections not found' + + # Get template from data scope. + _TEMPLATE = scope.data['directory_structure']['template'] + assert isinstance(_TEMPLATE, basestring), 'Invalid template' + + # Get template collections from data scope. + _COLLECTIONS = list() + for name in scope.data['directory_structure']['collections']: + _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) + assert _COLLECTIONS, 'Invalid collections' + + # Instantiate parser JIT. + _BUILDER = create_template_builder(_TEMPLATE, tuple(_COLLECTIONS), PARSING_STRICTNESS_1, separator='/') + + # Cached project. + _PROJECT = project + + for term in terms: + assert isinstance(term, Term), 'Invalid term :: {}'.format(term) + + return _BUILDER.build(terms) diff --git a/pyessv/_builders/filename.py b/pyessv/_builders/filename.py new file mode 100644 index 0000000..22f4fe9 --- /dev/null +++ b/pyessv/_builders/filename.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +""" +.. module:: pyessv._builders.dataset_id.py + :copyright: Copyright "December 01, 2016", IPSL + :license: GPL/CeCIL + :platform: Unix, Windows + :synopsis: Encapsulates building of an ESGF dataset identifier. + +.. moduleauthor:: Mark Conway-Greenslade + +""" +from pyessv._model.term import Term +from pyessv import all_scopes +from pyessv._constants import PARSING_STRICTNESS_1 +from pyessv._factory import create_template_builder +from pyessv._utils.compat import basestring + +# Instantiated template +_TEMPLATE = None + +# Instantiated template collections +_COLLECTIONS = None + +# Instantiated project. +_PROJECT = None + +# Instantiated builder. +_BUILDER = None + + +def build_filename(project, terms): + """Builds a filename. + + :param str project: Project code. + :param set terms: Filename terms. + + :returns: Filename string. + :rtype: str + + """ + assert isinstance(project, basestring), 'Invalid project' + assert isinstance(terms, set), 'Invalid terms' + + global _PROJECT, _BUILDER, _TEMPLATE, _COLLECTIONS + + if _PROJECT != project: + + # Get scope corresponding to the project code. + scopes = all_scopes() + assert project in [scope.name for scope in scopes], 'Unsupported project' + scope = [scope for scope in scopes if scope.name == project][0] + + assert 'filename' in scope.data.keys(), 'Filename parser not found' + assert 'template' in scope.data['filename'].keys(), 'Filename parser template not found' + assert 'collections' in scope.data['filename'].keys(), 'Filename parser template collections not found' + + # Get template from data scope. + _TEMPLATE = scope.data['filename']['template'] + assert isinstance(_TEMPLATE, basestring), 'Invalid template' + + # Get template collections from data scope. + _COLLECTIONS = list() + for name in scope.data['filename']['collections']: + _COLLECTIONS.append([collection.namespace for collection in scope.collections if collection.name == name.replace('_','-')][0]) + assert _COLLECTIONS, 'Invalid collections' + + # Instantiate parser JIT. + _BUILDER = create_template_builder(_TEMPLATE, tuple(_COLLECTIONS), PARSING_STRICTNESS_1, separator='_') + + # Cached project. + _PROJECT = project + + for term in terms: + assert isinstance(term, Term), 'Invalid term :: {}'.format(term) + + return _BUILDER.build(terms) diff --git a/pyessv/_initializer.py b/pyessv/_initializer.py index 1a89df5..f75e842 100644 --- a/pyessv/_initializer.py +++ b/pyessv/_initializer.py @@ -22,76 +22,63 @@ from pyessv._utils import logger - def init(): - """Library initializer. - - """ - # Verify archive folder exists. - if not os.path.isdir(DIR_ARCHIVE): - raise EnvironmentError('{} directory does not exists'.format(DIR_ARCHIVE)) - - -def load_cv(authority=None): - authorities = [] - if authority: - authority = read(authority=authority) - authorities.append(authority) - cache(authority) - else: - for authority in read(): - authorities.append(authority) - cache(authority) - - # Mixin pseudo-constants. - _mixin_constants(authorities) - - # Set scope level accessor functions. - _mixin_scope_accessors(authorities) - - -# def _load_authorities(): -# """Loads vocabulary authorities from archive. -# -# """ -# logger.log('Loading vocabularies from {}:'.format(DIR_ARCHIVE)) -# authorities = [] -# for authority in read(): -# authorities.append(authority) -# logger.log('... loaded: {}'.format(authority)) -# cache(authority) -# -# return authorities - + """Library initializer. + + """ + # Verify archive folder exists. + if not os.path.isdir(DIR_ARCHIVE): + raise EnvironmentError('{} directory does not exists'.format(DIR_ARCHIVE)) + + +def load_cv(authority=None, scope=None): + authorities = [] + if authority: + if scope: + authority = read(authority=authority, scope=scope) + else: + authority = read(authority=authority) + authorities.append(authority) + cache(authority) + else: + for authority in read(): + authorities.append(authority) + cache(authority) + + # Mixin pseudo-constants. + _mixin_constants(authorities) + + # Set scope level accessor functions. + _mixin_scope_accessors(authorities) def _mixin_constants(authorities): - """Mixes in authorities as pseudo-constants to pyessv. + """Mixes in authorities as pseudo-constants to pyessv. - """ - for authority in authorities: - attr_name = authority.canonical_name.replace('-', '_').upper() - setattr(pyessv, attr_name, authority) + """ + for authority in authorities: + attr_name = authority.canonical_name.replace('-', '_').upper() + setattr(pyessv, attr_name, authority) def _mixin_scope_accessors(authorities): - """Mixes in scope level vocab accessors functions. - - """ - # In pyessv._accessors sub-package are modules that expose helper functions for accessing vocabularies, - # here we are ensuring that those functions are easily accessed. - targets = [] - for authority in authorities: - for scope in authority: - try: - accessor = ACCESSORS[authority.canonical_name][scope.canonical_name] - except KeyError: - pass - else: - targets.append((scope, accessor)) - - # Mixin accessor functions with scope. - for scope, accessor in targets: - funcs = [i for i in inspect.getmembers(accessor) - if inspect.isfunction(i[1]) and not i[0].startswith('_')] - for name, func in funcs: - setattr(scope, name, func) + """Mixes in scope level vocab accessors functions. + + """ + # In pyessv._accessors sub-package are modules that expose helper functions for accessing vocabularies, + # here we are ensuring that those functions are easily accessed. + targets = [] + for authority in authorities: + for scope in authority: + try: + accessor = ACCESSORS[authority.canonical_name][scope.canonical_name] + except KeyError: + pass + else: + targets.append((scope, accessor)) + + # Mixin accessor functions with scope. + for scope, accessor in targets: + funcs = [i for i in inspect.getmembers(accessor) + if inspect.isfunction(i[1]) and not i[0].startswith('_')] + for name, func in funcs: + setattr(scope, name, func) diff --git a/pyessv/_io_manager.py b/pyessv/_io_manager.py index 8d6c028..ee32985 100644 --- a/pyessv/_io_manager.py +++ b/pyessv/_io_manager.py @@ -71,7 +71,7 @@ def delete(target): pass -def read(archive_dir=DIR_ARCHIVE, authority=None): +def read(authority=None, scope=None, archive_dir=DIR_ARCHIVE): """Reads vocabularies from archive folder (~/.esdoc/pyessv-archive) upon file system. :returns: List of vocabulary authorities loaded from archive folder. @@ -80,15 +80,19 @@ def read(archive_dir=DIR_ARCHIVE, authority=None): """ if authority: assert '{}/{}'.format(archive_dir, authority), 'Invalid authority' + if scope: + assert '{}/{}/{}'.format(archive_dir, authority, scope), 'Invalid scope' + return _read_authority('{}/{}'.format(archive_dir, authority), scope) return _read_authority('{}/{}'.format(archive_dir, authority)) else: return [_read_authority(i) for i in glob.glob('{}/*'.format(archive_dir)) if isdir(i)] -def _read_authority(dpath): +def _read_authority(dpath, scope=None): """Reads authority CV data from file system. :param str dpath: Path to a directory to which an authority's vocabularies have been written. + :param str scope: Select a scope's vocabularies to load (default loads all scopes). :returns: Authority vocabulary data. :rtype: pyessv.Authority @@ -103,11 +107,18 @@ def _read_authority(dpath): # Read terms. term_cache = {} - for scope in authority: + try: + scope = [s for s in authority if s.name == scope][0] for collection in scope: for term in _read_terms(dpath, scope, collection, term_cache): term.collection = collection collection.terms.append(term) + except IndexError: + for scope in authority: + for collection in scope: + for term in _read_terms(dpath, scope, collection, term_cache): + term.collection = collection + collection.terms.append(term) # Set inter-term hierarchies. for term in term_cache.values(): diff --git a/pyessv/_model/node.py b/pyessv/_model/node.py index 68dd808..cea06c3 100644 --- a/pyessv/_model/node.py +++ b/pyessv/_model/node.py @@ -16,7 +16,7 @@ import arrow from pyessv._constants import NODE_TYPEKEY_SET -from pyessv._utils.compat import str +from pyessv._utils.compat import str, basestring from pyessv._utils.formatter import format_io_name from pyessv._utils.validation import assert_iterable from pyessv._utils.validation import assert_string @@ -61,6 +61,17 @@ def __getattr__(self, name): except KeyError: raise AttributeError('{} unknown attribute'.format(name)) + def __getstate__(self): + """Serializing method used by Pickle. + + """ + return self.__dict__ + + def __setstate__(self, d): + """Unserializing used by Pickle. + + """ + self.__dict__.update(d) @property def name(self): diff --git a/pyessv/_parser.py b/pyessv/_parser.py index 5221f8c..635a251 100644 --- a/pyessv/_parser.py +++ b/pyessv/_parser.py @@ -26,7 +26,7 @@ def parse( namespace, strictness=PARSING_STRICTNESS_2, - field='canonical_name' + field=None ): """Parses a namespace within a vocabulary hierachy. @@ -36,7 +36,8 @@ def parse( """ assert strictness in PARSING_STRICTNESS_SET, 'Invalid parsing strictness' - assert field in PARSING_NODE_FIELDS, 'Invalid field' + if field: + assert field in PARSING_NODE_FIELDS, 'Invalid field' # Set namespace ns = str(namespace).strip().split(':') @@ -63,8 +64,10 @@ def parse( node = load(namespace) target.set_node(node) - return getattr(target.node, field) - + if field: + return getattr(target.node, field) + else: + return target.node class _NodeInfo(object): """Information about a node whose name is being parsed. diff --git a/pyessv/_parsers/filename.py b/pyessv/_parsers/filename.py index 91f4ee0..1d0213d 100644 --- a/pyessv/_parsers/filename.py +++ b/pyessv/_parsers/filename.py @@ -17,6 +17,7 @@ from pyessv._constants import PARSING_STRICTNESS_1 from pyessv._exceptions import TemplateParsingError from pyessv._factory import create_template_parser +from pyessv._utils.compat import basestring # Template extracted from esgf ini file (for reference purpose only). _INI_PATTERN = '%(variable)s_%(cmor_table)s_%(model)s_%(experiment)s_%(ensemble)s[_%(period_start)s-%(period_end)s].nc' diff --git a/sh/writers/esgf/map.py b/sh/writers/esgf/map.py index 729400e..83c129a 100644 --- a/sh/writers/esgf/map.py +++ b/sh/writers/esgf/map.py @@ -33,6 +33,10 @@ import map_pmip3 import map_primavera import map_tamip +import warnings +from arrow.factory import ArrowParseWarning + +warnings.simplefilter("ignore", ArrowParseWarning) # Define command line options. _ARGS = argparse.ArgumentParser('Maps ESGF publisher ini files to normalized pyessv vocabulary format.') @@ -219,7 +223,7 @@ def _create_scope(authority, project): ) -def _create_collection(module, scope, collection_id, term_regex=None, term_composed=[]): +def _create_collection(module, scope, collection_id, term_regex=None): """Factory method to return vocabulary collection. """ @@ -233,7 +237,6 @@ def _create_collection(module, scope, collection_id, term_regex=None, term_compo collection.description = "ESGF publisher-config CV collection: ".format(collection_id), collection.label = collection_id.title().replace('_', ' ').replace('Rcm', 'RCM').replace('Cmor', 'CMOR') collection.term_regex = term_regex - collection.term_composed = term_composed collection.data = data return collection @@ -243,7 +246,6 @@ def _create_collection(module, scope, collection_id, term_regex=None, term_compo "ESGF publisher-config CV collection: ".format(collection_id), label=collection_id.title().replace('_', ' ').replace('Rcm', 'RCM').replace('Cmor', 'CMOR'), term_regex=term_regex, - term_composed=term_composed, data=data ) diff --git a/sh/writers/esgf/map_c3s_cmip5.py b/sh/writers/esgf/map_c3s_cmip5.py index fe2c53e..f4f4e09 100644 --- a/sh/writers/esgf/map_c3s_cmip5.py +++ b/sh/writers/esgf/map_c3s_cmip5.py @@ -14,12 +14,12 @@ # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('cmor_table', yield_comma_delimited_options), ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), ('experiment', yield_pipe_delimited_options), - ('institute', lambda: yield_institute), ('model', yield_comma_delimited_options), + ('institute', lambda: yield_institute), ('time_frequency', yield_comma_delimited_options), ('product', yield_comma_delimited_options), ('realm', yield_comma_delimited_options), @@ -27,7 +27,7 @@ ('variable', yield_comma_delimited_options), ('dataset_version', r'latest|^v[0-9]*$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Arbitrary data associated with a collection. COLLECTION_DATA = { diff --git a/sh/writers/esgf/map_c3s_cordex.py b/sh/writers/esgf/map_c3s_cordex.py index b5fdbb3..b233118 100644 --- a/sh/writers/esgf/map_c3s_cordex.py +++ b/sh/writers/esgf/map_c3s_cordex.py @@ -13,7 +13,7 @@ from utils import yield_pipe_delimited_options # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('domain', lambda: yield_domain), ('driving_model', yield_comma_delimited_options), ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), @@ -28,7 +28,7 @@ ('variable', yield_comma_delimited_options), ('dataset_version', r'latest|v^[0-9]*$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { diff --git a/sh/writers/esgf/map_cc4e.py b/sh/writers/esgf/map_cc4e.py index 4052980..dcd418e 100644 --- a/sh/writers/esgf/map_cc4e.py +++ b/sh/writers/esgf/map_cc4e.py @@ -12,7 +12,7 @@ from utils import yield_comma_delimited_options # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('work_package', yield_comma_delimited_options), ('product', yield_comma_delimited_options), ('source_type', yield_comma_delimited_options), @@ -24,7 +24,7 @@ ('variable', yield_comma_delimited_options), ('dataset_version', r'latest|^v[0-9]*$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { diff --git a/sh/writers/esgf/map_cmip5.py b/sh/writers/esgf/map_cmip5.py index 025acef..5169b55 100644 --- a/sh/writers/esgf/map_cmip5.py +++ b/sh/writers/esgf/map_cmip5.py @@ -13,21 +13,21 @@ from utils import yield_pipe_delimited_options # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('cmor_table', yield_comma_delimited_options), ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), ('experiment', yield_pipe_delimited_options), ('model', yield_comma_delimited_options), ('institute', lambda: yield_institute), - ('las_time_delta', lambda: yield_las_time_delta), ('time_frequency', yield_comma_delimited_options), + ('las_time_delta', lambda: yield_las_time_delta), ('product', yield_comma_delimited_options), ('realm', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), ('dataset_version', r'latest|^v[0-9]*$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Arbitrary data associated with a collection. COLLECTION_DATA = { diff --git a/sh/writers/esgf/map_cmip6.py b/sh/writers/esgf/map_cmip6.py index b29df99..1234d22 100644 --- a/sh/writers/esgf/map_cmip6.py +++ b/sh/writers/esgf/map_cmip6.py @@ -12,11 +12,11 @@ from utils import yield_comma_delimited_options # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('las_time_delta', lambda: yield_las_time_delta), ('model_cohort', lambda: yield_model_cohort), ('thredds_exclude_variables', yield_comma_delimited_options), -} +] # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { diff --git a/sh/writers/esgf/map_cordex.py b/sh/writers/esgf/map_cordex.py index 925ba3a..d861e65 100644 --- a/sh/writers/esgf/map_cordex.py +++ b/sh/writers/esgf/map_cordex.py @@ -14,23 +14,23 @@ # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('domain', lambda: yield_domain), ('driving_model', yield_comma_delimited_options), ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), ('experiment', yield_pipe_delimited_options), ('institute', yield_comma_delimited_options), + ('time_frequency', yield_comma_delimited_options), ('las_time_delta', lambda: yield_las_time_delta), ('product', yield_comma_delimited_options), - ('rcm_name', lambda: yield_rcm_name), ('rcm_model', yield_comma_delimited_options), + ('rcm_name', lambda: yield_rcm_name), ('rcm_version', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), - ('time_frequency', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), ('dataset_version', r'latest|^v[0-9]*$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { diff --git a/sh/writers/esgf/map_cordex_adjust.py b/sh/writers/esgf/map_cordex_adjust.py index a2f1ab3..561d84c 100644 --- a/sh/writers/esgf/map_cordex_adjust.py +++ b/sh/writers/esgf/map_cordex_adjust.py @@ -14,23 +14,23 @@ # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('domain', lambda: yield_domain), ('driving_model', yield_comma_delimited_options), ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), ('experiment', yield_pipe_delimited_options), ('institute', yield_comma_delimited_options), + ('time_frequency', yield_comma_delimited_options), ('las_time_delta', lambda: yield_las_time_delta), ('product', yield_comma_delimited_options), ('rcm_model', yield_comma_delimited_options), ('rcm_name', lambda: yield_rcm_name), ('bias_adjustment', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), - ('time_frequency', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), ('dataset_version', r'latest|v^[0-9]*$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { diff --git a/sh/writers/esgf/map_euclipse.py b/sh/writers/esgf/map_euclipse.py index 912d20e..8afa008 100644 --- a/sh/writers/esgf/map_euclipse.py +++ b/sh/writers/esgf/map_euclipse.py @@ -14,21 +14,21 @@ # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('cmor_table', yield_comma_delimited_options), ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), ('experiment', yield_pipe_delimited_options), - ('institute', lambda: yield_institute), - ('las_time_delta', lambda: yield_las_time_delta), - ('model', yield_comma_delimited_options), - ('time_frequency', yield_comma_delimited_options), + ('model', yield_comma_delimited_options), + ('institute', lambda: yield_institute), + ('time_frequency', yield_comma_delimited_options), + ('las_time_delta', lambda: yield_las_time_delta), ('product', yield_comma_delimited_options), ('realm', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), ('dataset_version', r'latest|^v[0-9]*$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Fields extracted from ini file & appended as data to the scope. diff --git a/sh/writers/esgf/map_geomip.py b/sh/writers/esgf/map_geomip.py index 644cc52..5ca8516 100644 --- a/sh/writers/esgf/map_geomip.py +++ b/sh/writers/esgf/map_geomip.py @@ -14,21 +14,21 @@ # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('cmor_table', yield_comma_delimited_options), ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), ('experiment', yield_pipe_delimited_options), - ('institute', lambda: yield_institute), - ('las_time_delta', lambda: yield_las_time_delta), ('model', yield_comma_delimited_options), + ('institute', lambda: yield_institute), ('time_frequency', yield_comma_delimited_options), + ('las_time_delta', lambda: yield_las_time_delta), ('product', yield_comma_delimited_options), ('realm', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), ('dataset_version', r'latest|^v[0-9]*$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { diff --git a/sh/writers/esgf/map_input4mips.py b/sh/writers/esgf/map_input4mips.py index 6cb2271..4b9a77e 100644 --- a/sh/writers/esgf/map_input4mips.py +++ b/sh/writers/esgf/map_input4mips.py @@ -13,7 +13,7 @@ from utils import yield_comma_delimited_options # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('variable_id', lambda: yield_variable_id_options), ('activity_id', yield_comma_delimited_options), ('dataset_category', r'^[A-Za-z0-9]*$'), @@ -27,7 +27,7 @@ ('thredds_exclude_variables', yield_comma_delimited_options), ('dataset_version', r'latest|^v[0_9]{8}$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { diff --git a/sh/writers/esgf/map_isimip_ft.py b/sh/writers/esgf/map_isimip_ft.py index 00c5180..d2b2891 100644 --- a/sh/writers/esgf/map_isimip_ft.py +++ b/sh/writers/esgf/map_isimip_ft.py @@ -14,7 +14,7 @@ # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('product', yield_comma_delimited_options), ('model', yield_comma_delimited_options), ('impact_model', yield_comma_delimited_options), @@ -30,7 +30,7 @@ ('dataset_version', r'latest|^v[0-9]*$'), ('thredds_exclude_variables', yield_comma_delimited_options), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { diff --git a/sh/writers/esgf/map_lucid.py b/sh/writers/esgf/map_lucid.py index c2ec6ea..556907a 100644 --- a/sh/writers/esgf/map_lucid.py +++ b/sh/writers/esgf/map_lucid.py @@ -14,21 +14,21 @@ # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('cmor_table', yield_comma_delimited_options), ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), ('experiment', yield_pipe_delimited_options), - ('institute', lambda: yield_institute), - ('las_time_delta', lambda: yield_las_time_delta), ('model', yield_comma_delimited_options), + ('institute', lambda: yield_institute), ('time_frequency', yield_comma_delimited_options), + ('las_time_delta', lambda: yield_las_time_delta), ('product', yield_comma_delimited_options), ('realm', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), ('dataset_version', r'latest|^v[0-9]*$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { diff --git a/sh/writers/esgf/map_obs4mips.py b/sh/writers/esgf/map_obs4mips.py index 808f5d5..07b56b0 100644 --- a/sh/writers/esgf/map_obs4mips.py +++ b/sh/writers/esgf/map_obs4mips.py @@ -12,10 +12,8 @@ from utils import yield_comma_delimited_options -# TODO process map: las_time_delta_map = map(time_frequency : las_time_delta) - # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('product', yield_comma_delimited_options), ('institute', yield_comma_delimited_options), ('realm', yield_comma_delimited_options), @@ -30,37 +28,43 @@ ('las_time_delta', lambda: yield_las_time_delta), ('thredds_exclude_variables', yield_comma_delimited_options), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { - 'filename_template': '{}_{}_{}_{}_{}', - 'filename_collections': ( - 'variable', - 'source_id', - 'processing_level', - 'processing_version', - 'file_period' - ), - 'directory_template': 'obs4MIPs/{}/{}/{}/{}/{}/{}/{}/{}', - 'directory_collections': ( - 'product', - 'realm', - 'var', - 'time_frequency', - 'data_structure', - 'institute', - 'source_id', - 'dataset_version' - ), - 'dataset_id_template': 'obs4MIPs.{}.{}.{}.{}.{}', - 'dataset_id_collections': ( - 'institute', - 'source_id', - 'variable', - 'time_frequency', - 'dataset_version' - ) + 'filename': { + 'template': '{}_{}_{}_{}_{}', + 'collections': ( + 'variable', + 'source_id', + 'processing_level', + 'processing_version', + 'file_period' + ) + }, + 'directory_structure': { + 'template': 'obs4MIPs/{}/{}/{}/{}/{}/{}/{}/{}', + 'collections': ( + 'product', + 'realm', + 'var', + 'time_frequency', + 'data_structure', + 'institute', + 'source_id', + 'dataset_version' + ) + }, + 'dataset_id': { + 'template': 'obs4MIPs.{}.{}.{}.{}.{}', + 'collections': ( + 'institute', + 'source_id', + 'variable', + 'time_frequency', + 'dataset_version' + ) + } } diff --git a/sh/writers/esgf/map_pmip3.py b/sh/writers/esgf/map_pmip3.py index dd6f125..fd727e6 100644 --- a/sh/writers/esgf/map_pmip3.py +++ b/sh/writers/esgf/map_pmip3.py @@ -14,21 +14,21 @@ # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('cmor_table', yield_comma_delimited_options), ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), ('experiment', yield_pipe_delimited_options), - ('institute', lambda: yield_institute), - ('las_time_delta', lambda: yield_las_time_delta), ('model', yield_comma_delimited_options), + ('institute', lambda: yield_institute), ('time_frequency', yield_comma_delimited_options), + ('las_time_delta', lambda: yield_las_time_delta), ('product', yield_comma_delimited_options), ('realm', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), ('dataset_version', r'latest|^v[0-9]*$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { diff --git a/sh/writers/esgf/map_primavera.py b/sh/writers/esgf/map_primavera.py index 4a7b509..b7a0d39 100644 --- a/sh/writers/esgf/map_primavera.py +++ b/sh/writers/esgf/map_primavera.py @@ -13,10 +13,10 @@ from utils import yield_pipe_delimited_options # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('activity', yield_comma_delimited_options), - ('institute', lambda: yield_institute), ('model', yield_comma_delimited_options), + ('institute', lambda: yield_institute), ('experiment', yield_pipe_delimited_options), ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+f[0-9]+'), ('cmor_table', yield_comma_delimited_options), @@ -25,7 +25,7 @@ ('thredds_exclude_variables', yield_comma_delimited_options), ('dataset_version', r'latest|^v[0-9]*$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { diff --git a/sh/writers/esgf/map_tamip.py b/sh/writers/esgf/map_tamip.py index a8ff928..6687ac0 100644 --- a/sh/writers/esgf/map_tamip.py +++ b/sh/writers/esgf/map_tamip.py @@ -14,21 +14,21 @@ # Vocabulary collections extracted from ini file. -COLLECTIONS = { +COLLECTIONS = [ ('cmor_table', yield_comma_delimited_options), ('ensemble', r'r[0-9]+i[0-9]+p[0-9]+'), ('experiment', yield_pipe_delimited_options), - ('institute', lambda: yield_institute), - ('las_time_delta', lambda: yield_las_time_delta), ('model', yield_comma_delimited_options), + ('institute', lambda: yield_institute), ('time_frequency', yield_comma_delimited_options), + ('las_time_delta', lambda: yield_las_time_delta), ('product', yield_comma_delimited_options), ('realm', yield_comma_delimited_options), ('thredds_exclude_variables', yield_comma_delimited_options), ('variable', yield_comma_delimited_options), ('dataset_version', r'latest|^v[0-9]*$'), ('file_period', r'fixed|^\d+-\d+(-clim)?$') -} +] # Fields extracted from ini file & appended as data to the scope. SCOPE_DATA = { diff --git a/sh/writers/wcrp/cmip6/write.py b/sh/writers/wcrp/cmip6/write.py index 20f4f6d..5798954 100644 --- a/sh/writers/wcrp/cmip6/write.py +++ b/sh/writers/wcrp/cmip6/write.py @@ -322,7 +322,7 @@ def _get_institution_data(_, name): """ obj = _INSTITUTIONAL_DATA.get(name, {}) - del obj['code'] + #del obj['code'] return obj