""" Configuration object required to rund a Mapchete process. Before running a process, a MapcheteConfig object has to be initialized by either using a Mapchete file or a dictionary holding the process parameters. Upon creation, all parameters are validated and the InputData objects are created which are then exposed to the user process. An invalid process configuration or an invalid process file cause an Exception when initializing the configuration. """ from cached_property import cached_property from collections import OrderedDict from copy import deepcopy import imp import importlib import inspect import logging import operator import os import oyaml as yaml import py_compile from shapely import wkt from shapely.geometry import box from shapely.ops import cascaded_union from tilematrix._funcs import Bounds import warnings from mapchete.validate import ( validate_bounds, validate_zooms, validate_values, validate_bufferedtilepyramid ) from mapchete.errors import ( MapcheteConfigError, MapcheteProcessSyntaxError, MapcheteProcessImportError, MapcheteDriverError ) from mapchete.formats import ( load_output_reader, load_output_writer, available_output_formats, load_input_reader ) from mapchete.io import absolute_path from mapchete.log import add_module_logger from mapchete.tile import BufferedTilePyramid logger = logging.getLogger(__name__) # parameters which have to be provided in the configuration and their types _MANDATORY_PARAMETERS = [ ("process", str), # path to .py file or module path ("pyramid", dict), # process pyramid definition ("input", (dict, type(None))), # files & other types ("output", dict), # process output parameters ("zoom_levels", (int, dict, list)) # process zoom levels ] # parameters with special functions which cannot be used for user parameters _RESERVED_PARAMETERS = [ "baselevels", # enable interpolation from other zoom levels "bounds", # process bounds "process", # path to .py file or module path "config_dir", # configuration base directory "process_minzoom", # minimum zoom where process is valid (deprecated) "process_maxzoom", # maximum zoom where process is valid (deprecated) "process_zoom", # single zoom where process is valid (deprecated) "process_bounds", # process boundaries (deprecated) "metatiling", # process metatile size (deprecated) "pixelbuffer", # buffer around each tile in pixels (deprecated) ] class MapcheteConfig(object): """ Process configuration. MapcheteConfig reads and parses a Mapchete configuration, verifies the parameters, creates the necessary metadata required and provides the configuration snapshot for every zoom level. Parameters ---------- input_config : string or dictionary a Mapchete configuration file or a configuration dictionary zoom : list or integer zoom level or a pair of minimum and maximum zoom level the process is initialized with bounds : tuple left, bottom, right, top boundaries the process is initalized with single_input_file : string single input file if supported by process mode : string * ``memory``: Generate process output on demand without reading pre-existing data or writing new data. * ``readonly``: Just read data without processing new data. * ``continue``: (default) Don't overwrite existing output. * ``overwrite``: Overwrite existing output. Attributes ---------- mode : string process mode process : string absolute path to process file config_dir : string path to configuration directory process_pyramid : ``tilematrix.TilePyramid`` ``TilePyramid`` used to process data output_pyramid : ``tilematrix.TilePyramid`` ``TilePyramid`` used to write output data input : dictionary inputs for process output : ``OutputData`` driver specific output object zoom_levels : list process zoom levels bounds : tuple process bounds init_zoom_levels : list zoom levels the process configuration was initialized with init_bounds : tuple bounds the process configuration was initialized with baselevels : dictionary base zoomlevels, where data is processed; zoom levels not included are generated from baselevels Deprecated Attributes --------------------- raw : dictionary raw process configuration mapchete_file : string path to Mapchete file output_type : string (moved to OutputData) process output type (``raster`` or ``vector``) crs : ``rasterio.crs.CRS`` (moved to process_pyramid) object describing the process coordinate reference system pixelbuffer : integer (moved to process_pyramid) buffer around process tiles metatiling : integer (moved to process_pyramid) process metatiling """ def __init__( self, input_config, zoom=None, bounds=None, single_input_file=None, mode="continue", debug=False ): """Initialize configuration.""" # get dictionary representation of input_config and # (0) map deprecated params to new structure self._raw = _map_to_new_config(_config_to_dict(input_config)) self._raw["init_zoom_levels"] = zoom self._raw["init_bounds"] = bounds self._cache_area_at_zoom = {} self._cache_full_process_area = None # (1) assert mandatory params are available try: validate_values(self._raw, _MANDATORY_PARAMETERS) except Exception as e: raise MapcheteConfigError(e) # (2) check user process logger.debug("validating process code") self.config_dir = self._raw["config_dir"] self.process_name = self.process_path = self._raw["process"] self.process_func # (3) set process and output pyramids logger.debug("initializing pyramids") try: process_metatiling = self._raw["pyramid"].get("metatiling", 1) # output metatiling defaults to process metatiling if not set # explicitly output_metatiling = self._raw["output"].get( "metatiling", process_metatiling) # we cannot properly handle output tiles which are bigger than # process tiles if output_metatiling > process_metatiling: raise ValueError( "output metatiles must be smaller than process metatiles") # these two BufferedTilePyramid instances will help us with all # the tile geometries etc. self.process_pyramid = BufferedTilePyramid( self._raw["pyramid"]["grid"], metatiling=process_metatiling, pixelbuffer=self._raw["pyramid"].get("pixelbuffer", 0)) self.output_pyramid = BufferedTilePyramid( self._raw["pyramid"]["grid"], metatiling=output_metatiling, pixelbuffer=self._raw["output"].get("pixelbuffer", 0)) except Exception as e: logger.exception(e) raise MapcheteConfigError(e) # (4) set mode if mode not in ["memory", "continue", "readonly", "overwrite"]: raise MapcheteConfigError("unknown mode %s" % mode) self.mode = mode # don't inititalize inputs on readonly mode or if only overviews are going to be # built self._init_inputs = False if ( self.mode == "readonly" or ( not len( set(self.baselevels["zooms"]).intersection(set(self.init_zoom_levels)) ) if self.baselevels else False ) ) else True # (5) prepare process parameters per zoom level without initializing # input and output classes logger.debug("preparing process parameters") self._params_at_zoom = _raw_at_zoom(self._raw, self.init_zoom_levels) # (6) the delimiters are used by some input drivers self._delimiters = dict( zoom=self.init_zoom_levels, bounds=self.init_bounds, process_bounds=self.bounds, effective_bounds=self.effective_bounds ) # (7) initialize output logger.debug("initializing output") self.output # (8) initialize input items # depending on the inputs this action takes the longest and is done # in the end to let all other actions fail earlier if necessary logger.debug("initializing input") self.input # (9) some output drivers such as the GeoTIFF single file driver also needs the # process area to prepare logger.debug("prepare output") self.output.prepare(process_area=self.area_at_zoom()) @cached_property def zoom_levels(self): """Process zoom levels as defined in the configuration.""" return validate_zooms(self._raw["zoom_levels"]) @cached_property def init_zoom_levels(self): """ Zoom levels this process is currently initialized with. This gets triggered by using the ``zoom`` kwarg. If not set, it will be equal to self.zoom_levels. """ try: return get_zoom_levels( process_zoom_levels=self._raw["zoom_levels"], init_zoom_levels=self._raw["init_zoom_levels"] ) except Exception as e: raise MapcheteConfigError(e) @cached_property def bounds(self): """Process bounds as defined in the configuration.""" if self._raw["bounds"] is None: return self.process_pyramid.bounds else: try: return validate_bounds(self._raw["bounds"]) except Exception as e: raise MapcheteConfigError(e) @cached_property def init_bounds(self): """ Process bounds this process is currently initialized with. This gets triggered by using the ``init_bounds`` kwarg. If not set, it will be equal to self.bounds. """ if self._raw["init_bounds"] is None: return self.bounds else: try: return validate_bounds(self._raw["init_bounds"]) except Exception as e: raise MapcheteConfigError(e) @cached_property def effective_bounds(self): """ Effective process bounds required to initialize inputs. Process bounds sometimes have to be larger, because all intersecting process tiles have to be covered as well. """ return snap_bounds( bounds=clip_bounds(bounds=self.init_bounds, clip=self.process_pyramid.bounds), pyramid=self.process_pyramid, zoom=min( self.baselevels["zooms"] ) if self.baselevels else min( self.init_zoom_levels ) ) @cached_property def _output_params(self): """Output params of driver.""" output_params = dict( self._raw["output"], grid=self.output_pyramid.grid, pixelbuffer=self.output_pyramid.pixelbuffer, metatiling=self.output_pyramid.metatiling, delimiters=self._delimiters, mode=self.mode ) if "path" in output_params: output_params.update( path=absolute_path(path=output_params["path"], base_dir=self.config_dir) ) if "format" not in output_params: raise MapcheteConfigError("output format not specified") if output_params["format"] not in available_output_formats(): raise MapcheteConfigError( "format %s not available in %s" % ( output_params["format"], str(available_output_formats()) ) ) return output_params @cached_property def output(self): """Output writer class of driver.""" writer = load_output_writer(self._output_params) try: writer.is_valid_with_config(self._output_params) except Exception as e: logger.exception(e) raise MapcheteConfigError( "driver %s not compatible with configuration: %s" % ( writer.METADATA["driver_name"], e ) ) return writer @cached_property def output_reader(self): """Output reader class of driver.""" if self.baselevels: return load_output_reader(self._output_params) else: return self.output @cached_property def input(self): """ Input items used for process stored in a dictionary. Keys are the hashes of the input parameters, values the respective InputData classes. If process mode is `readonly` or if only overviews are about to be built, no inputs are required and thus not initialized due to performance reasons. However, process bounds which otherwise are dependant on input bounds, may change if not explicitly provided in process configuration. """ # get input items only of initialized zoom levels raw_inputs = OrderedDict([ # convert input definition to hash (get_hash(v), v) for zoom in self.init_zoom_levels if "input" in self._params_at_zoom[zoom] # to preserve file groups, "flatten" the input tree and use # the tree paths as keys for key, v in _flatten_tree(self._params_at_zoom[zoom]["input"]) if v is not None ]) initalized_inputs = OrderedDict() if self._init_inputs: for k, v in raw_inputs.items(): # for files and tile directories if isinstance(v, str): logger.debug("load input reader for simple input %s", v) try: reader = load_input_reader( dict( path=absolute_path(path=v, base_dir=self.config_dir), pyramid=self.process_pyramid, pixelbuffer=self.process_pyramid.pixelbuffer, delimiters=self._delimiters ), readonly=self.mode == "readonly" ) except Exception as e: logger.exception(e) raise MapcheteDriverError( "error when loading input %s: %s" % (v, e) ) logger.debug("input reader for simple input %s is %s", v, reader) # for abstract inputs elif isinstance(v, dict): logger.debug("load input reader for abstract input %s", v) try: reader = load_input_reader( dict( abstract=deepcopy(v), pyramid=self.process_pyramid, pixelbuffer=self.process_pyramid.pixelbuffer, delimiters=self._delimiters, conf_dir=self.config_dir ), readonly=self.mode == "readonly" ) except Exception as e: logger.exception(e) raise MapcheteDriverError( "error when loading input %s: %s" % (v, e) ) logger.debug("input reader for abstract input %s is %s", v, reader) else: raise MapcheteConfigError("invalid input type %s", type(v)) # trigger bbox creation reader.bbox(out_crs=self.process_pyramid.crs) initalized_inputs[k] = reader else: for k in raw_inputs.keys(): initalized_inputs[k] = None return initalized_inputs @cached_property def baselevels(self): """ Optional baselevels configuration. baselevels: min: <zoom> max: <zoom> lower: <resampling method> higher: <resampling method> """ if "baselevels" not in self._raw: return {} baselevels = self._raw["baselevels"] minmax = {k: v for k, v in baselevels.items() if k in ["min", "max"]} if not minmax: raise MapcheteConfigError("no min and max values given for baselevels") for v in minmax.values(): if not isinstance(v, int) or v < 0: raise MapcheteConfigError( "invalid baselevel zoom parameter given: %s" % minmax.values() ) zooms = list(range( minmax.get("min", min(self.zoom_levels)), minmax.get("max", max(self.zoom_levels)) + 1) ) if not set(self.zoom_levels).difference(set(zooms)): raise MapcheteConfigError("baselevels zooms fully cover process zooms") return dict( zooms=zooms, lower=baselevels.get("lower", "nearest"), higher=baselevels.get("higher", "nearest"), tile_pyramid=BufferedTilePyramid( self.output_pyramid.grid, pixelbuffer=self.output_pyramid.pixelbuffer, metatiling=self.process_pyramid.metatiling ) ) @cached_property def process_func(self): return get_process_func( process_path=self.process_path, config_dir=self.config_dir, run_compile=True ) def get_process_func_params(self, zoom): return { k: v for k, v in self.params_at_zoom(zoom).items() if k in inspect.signature(self.process_func).parameters } def get_inputs_for_tile(self, tile): def _open_inputs(i): for k, v in i.items(): if v is None: continue elif isinstance(v, dict): yield (k, list(_open_inputs(v))) else: yield (k, v.open(tile)) return OrderedDict(list(_open_inputs(self.params_at_zoom(tile.zoom)["input"]))) def params_at_zoom(self, zoom): """ Return configuration parameters snapshot for zoom as dictionary. Parameters ---------- zoom : int zoom level Returns ------- configuration snapshot : dictionary zoom level dependent process configuration """ if zoom not in self.init_zoom_levels: raise ValueError("zoom level not available with current configuration") out = OrderedDict( self._params_at_zoom[zoom], input=OrderedDict(), output=self.output ) if "input" in self._params_at_zoom[zoom]: flat_inputs = OrderedDict() for k, v in _flatten_tree(self._params_at_zoom[zoom]["input"]): if v is None: flat_inputs[k] = None else: flat_inputs[k] = self.input[get_hash(v)] out["input"] = _unflatten_tree(flat_inputs) else: out["input"] = OrderedDict() return out def area_at_zoom(self, zoom=None): """ Return process bounding box for zoom level. Parameters ---------- zoom : int or None if None, the union of all zoom level areas is returned Returns ------- process area : shapely geometry """ if not self._init_inputs: return box(*self.init_bounds) if zoom is None: if not self._cache_full_process_area: logger.debug("calculate process area ...") self._cache_full_process_area = cascaded_union([ self._area_at_zoom(z) for z in self.init_zoom_levels] ).buffer(0) return self._cache_full_process_area else: if zoom not in self.init_zoom_levels: raise ValueError("zoom level not available with current configuration") return self._area_at_zoom(zoom) def _area_at_zoom(self, zoom): if zoom not in self._cache_area_at_zoom: # use union of all input items and, if available, intersect with # init_bounds if "input" in self._params_at_zoom[zoom]: input_union = cascaded_union([ self.input[get_hash(v)].bbox(self.process_pyramid.crs) for k, v in _flatten_tree(self._params_at_zoom[zoom]["input"]) if v is not None ]) self._cache_area_at_zoom[zoom] = input_union.intersection( box(*self.init_bounds) ) if self.init_bounds else input_union # if no input items are available, just use init_bounds else: self._cache_area_at_zoom[zoom] = box(*self.init_bounds) return self._cache_area_at_zoom[zoom] def bounds_at_zoom(self, zoom=None): """ Return process bounds for zoom level. Parameters ---------- zoom : integer or list Returns ------- process bounds : tuple left, bottom, right, top """ return ( () if self.area_at_zoom(zoom).is_empty else Bounds(*self.area_at_zoom(zoom).bounds) ) # deprecated: ############# @cached_property def crs(self): """Deprecated.""" warnings.warn(DeprecationWarning("self.crs is now self.process_pyramid.crs.")) return self.process_pyramid.crs @cached_property def metatiling(self): """Deprecated.""" warnings.warn( DeprecationWarning("self.metatiling is now self.process_pyramid.metatiling.") ) return self.process_pyramid.metatiling @cached_property def pixelbuffer(self): """Deprecated.""" warnings.warn( DeprecationWarning( "self.pixelbuffer is now self.process_pyramid.pixelbuffer." ) ) return self.process_pyramid.pixelbuffer @cached_property def inputs(self): """Deprecated.""" warnings.warn(DeprecationWarning("self.inputs renamed to self.input.")) return self.input @cached_property def process_file(self): """Deprecated.""" warnings.warn(DeprecationWarning("'self.process_file' is deprecated")) return os.path.join(self._raw["config_dir"], self._raw["process"]) def at_zoom(self, zoom): """Deprecated.""" warnings.warn(DeprecationWarning("Method renamed to self.params_at_zoom(zoom).")) return self.params_at_zoom(zoom) def process_area(self, zoom=None): """Deprecated.""" warnings.warn(DeprecationWarning("Method renamed to self.area_at_zoom(zoom).")) return self.area_at_zoom(zoom) def process_bounds(self, zoom=None): """Deprecated.""" warnings.warn(DeprecationWarning("Method renamed to self.bounds_at_zoom(zoom).")) return self.bounds_at_zoom(zoom) def get_hash(x): """Return hash of x.""" if isinstance(x, str): return hash(x) elif isinstance(x, dict): return hash(yaml.dump(x)) def get_zoom_levels(process_zoom_levels=None, init_zoom_levels=None): """Validate and return zoom levels.""" process_zoom_levels = validate_zooms(process_zoom_levels) if init_zoom_levels is None: return process_zoom_levels else: init_zoom_levels = validate_zooms(init_zoom_levels) if not set(init_zoom_levels).issubset(set(process_zoom_levels)): raise ValueError("init zooms must be a subset of process zoom") return init_zoom_levels def snap_bounds(bounds=None, pyramid=None, zoom=None): """ Snaps bounds to tiles boundaries of specific zoom level. Parameters ---------- bounds : bounds to be snapped pyramid : TilePyramid zoom : int Returns ------- Bounds(left, bottom, right, top) """ bounds = validate_bounds(bounds) pyramid = validate_bufferedtilepyramid(pyramid) lb = pyramid.tile_from_xy(bounds.left, bounds.bottom, zoom, on_edge_use="rt").bounds rt = pyramid.tile_from_xy(bounds.right, bounds.top, zoom, on_edge_use="lb").bounds return Bounds(lb.left, lb.bottom, rt.right, rt.top) def clip_bounds(bounds=None, clip=None): """ Clips bounds by clip. Parameters ---------- bounds : bounds to be clipped clip : clip bounds Returns ------- Bounds(left, bottom, right, top) """ bounds = validate_bounds(bounds) clip = validate_bounds(clip) return Bounds( max(bounds.left, clip.left), max(bounds.bottom, clip.bottom), min(bounds.right, clip.right), min(bounds.top, clip.top) ) def raw_conf(mapchete_file): """ Loads a mapchete_file into a dictionary. Parameters ---------- mapchete_file : str Path to a Mapchete file. Returns ------- dictionary """ if isinstance(mapchete_file, dict): return _map_to_new_config(mapchete_file) else: return _map_to_new_config(yaml.safe_load(open(mapchete_file, "r").read())) def raw_conf_process_pyramid(raw_conf): """ Loads the process pyramid of a raw configuration. Parameters ---------- raw_conf : dict Raw mapchete configuration as dictionary. Returns ------- BufferedTilePyramid """ return BufferedTilePyramid( raw_conf["pyramid"]["grid"], metatiling=raw_conf["pyramid"].get("metatiling", 1), pixelbuffer=raw_conf["pyramid"].get("pixelbuffer", 0) ) def raw_conf_output_pyramid(raw_conf): """ Loads the process pyramid of a raw configuration. Parameters ---------- raw_conf : dict Raw mapchete configuration as dictionary. Returns ------- BufferedTilePyramid """ return BufferedTilePyramid( raw_conf["pyramid"]["grid"], metatiling=raw_conf["output"].get( "metatiling", raw_conf["pyramid"].get("metatiling", 1) ), pixelbuffer=raw_conf["pyramid"].get( "pixelbuffer", raw_conf["pyramid"].get("pixelbuffer", 0) ) ) def bounds_from_opts( wkt_geometry=None, point=None, bounds=None, zoom=None, raw_conf=None ): """ Loads the process pyramid of a raw configuration. Parameters ---------- raw_conf : dict Raw mapchete configuration as dictionary. Returns ------- BufferedTilePyramid """ if wkt_geometry: return Bounds(*wkt.loads(wkt_geometry).bounds) elif point: x, y = point zoom_levels = get_zoom_levels( process_zoom_levels=raw_conf["zoom_levels"], init_zoom_levels=zoom ) tp = raw_conf_process_pyramid(raw_conf) return Bounds(*tp.tile_from_xy(x, y, max(zoom_levels)).bounds) else: return validate_bounds(bounds) if bounds is not None else bounds def get_process_func(process_path=None, config_dir=None, run_compile=False): logger.debug("get process function from %s", process_path) process_module = _load_process_module( process_path=process_path, config_dir=config_dir, run_compile=run_compile ) try: if hasattr(process_module, "Process"): logger.error( """instanciating MapcheteProcess is deprecated, """ """provide execute() function instead""" ) if hasattr(process_module, "execute"): return process_module.execute else: raise ImportError( "No execute() function found in %s" % process_path ) except ImportError as e: raise MapcheteProcessImportError(e) def _load_process_module(process_path=None, config_dir=None, run_compile=False): if process_path.endswith(".py"): abs_path = os.path.join(config_dir, process_path) if not os.path.isfile(abs_path): raise MapcheteConfigError("%s is not available" % abs_path) try: if run_compile: py_compile.compile(abs_path, doraise=True) module = imp.load_source( os.path.splitext(os.path.basename(abs_path))[0], abs_path ) # configure process file logger add_module_logger(module.__name__) except py_compile.PyCompileError as e: raise MapcheteProcessSyntaxError(e) except ImportError as e: raise MapcheteProcessImportError(e) else: try: module = importlib.import_module(process_path) except ImportError as e: raise MapcheteProcessImportError(e) return module def _config_to_dict(input_config): if isinstance(input_config, dict): if "config_dir" not in input_config: raise MapcheteConfigError("config_dir parameter missing") return OrderedDict(input_config, mapchete_file=None) # from Mapchete file elif os.path.splitext(input_config)[1] == ".mapchete": with open(input_config, "r") as config_file: return OrderedDict( yaml.safe_load(config_file.read()), config_dir=os.path.dirname(os.path.realpath(input_config)), mapchete_file=input_config ) # throw error if unknown object else: raise MapcheteConfigError( "Configuration has to be a dictionary or a .mapchete file.") def _raw_at_zoom(config, zooms): """Return parameter dictionary per zoom level.""" params_per_zoom = OrderedDict() for zoom in zooms: params = OrderedDict() for name, element in config.items(): if name not in _RESERVED_PARAMETERS: out_element = _element_at_zoom(name, element, zoom) if out_element is not None: params[name] = out_element params_per_zoom[zoom] = params return OrderedDict(params_per_zoom) def _element_at_zoom(name, element, zoom): """ Return the element filtered by zoom level. - An input integer or float gets returned as is. - An input string is checked whether it starts with "zoom". Then, the provided zoom level gets parsed and compared with the actual zoom level. If zoom levels match, the element gets returned. TODOs/gotchas: - Provided zoom levels for one element in config file are not allowed to "overlap", i.e. there is not yet a decision mechanism implemented which handles this case. """ # If element is a dictionary, analyze subitems. if isinstance(element, dict): if "format" in element: # we have an input or output driver here return element out_elements = OrderedDict() for sub_name, sub_element in element.items(): out_element = _element_at_zoom(sub_name, sub_element, zoom) if name == "input": out_elements[sub_name] = out_element elif out_element is not None: out_elements[sub_name] = out_element # If there is only one subelement, collapse unless it is # input. In such case, return a dictionary. if len(out_elements) == 1 and name != "input": return next(iter(out_elements.values())) # If subelement is empty, return None if len(out_elements) == 0: return None return out_elements # If element is a zoom level statement, filter element. elif isinstance(name, str): if name.startswith("zoom"): return _filter_by_zoom( conf_string=name.strip("zoom").strip(), zoom=zoom, element=element) # If element is a string but not a zoom level statement, return # element. else: return element # Return all other types as they are. else: return element def _filter_by_zoom(element=None, conf_string=None, zoom=None): """Return element only if zoom condition matches with config string.""" for op_str, op_func in [ # order of operators is important: # prematurely return in cases of "<=" or ">=", otherwise # _strip_zoom() cannot parse config strings starting with "<" # or ">" ("=", operator.eq), ("<=", operator.le), (">=", operator.ge), ("<", operator.lt), (">", operator.gt), ]: if conf_string.startswith(op_str): return element if op_func(zoom, _strip_zoom(conf_string, op_str)) else None def _strip_zoom(input_string, strip_string): """Return zoom level as integer or throw error.""" try: return int(input_string.strip(strip_string)) except Exception as e: raise MapcheteConfigError("zoom level could not be determined: %s" % e) def _flatten_tree(tree, old_path=None): """Flatten dict tree into dictionary where keys are paths of old dict.""" flat_tree = [] for key, value in tree.items(): new_path = "/".join([old_path, key]) if old_path else key if isinstance(value, dict) and "format" not in value: flat_tree.extend(_flatten_tree(value, old_path=new_path)) else: flat_tree.append((new_path, value)) return flat_tree def _unflatten_tree(flat): """Reverse tree flattening.""" tree = OrderedDict() for key, value in flat.items(): path = key.split("/") # we are at the end of a branch if len(path) == 1: tree[key] = value # there are more branches else: # create new dict if not path[0] in tree: tree[path[0]] = _unflatten_tree({"/".join(path[1:]): value}) # add keys to existing dict else: branch = _unflatten_tree({"/".join(path[1:]): value}) if not path[1] in tree[path[0]]: tree[path[0]][path[1]] = branch[path[1]] else: tree[path[0]][path[1]].update(branch[path[1]]) return tree def _map_to_new_config(config): try: validate_values(config, [("output", dict)]) except Exception as e: raise MapcheteConfigError(e) if "type" in config["output"]: warnings.warn(DeprecationWarning("'type' is deprecated and should be 'grid'")) if "grid" not in config["output"]: config["output"]["grid"] = config["output"].pop("type") if "pyramid" not in config: warnings.warn( DeprecationWarning("'pyramid' needs to be defined in root config element.") ) config["pyramid"] = dict( grid=config["output"]["grid"], metatiling=config.get("metatiling", 1), pixelbuffer=config.get("pixelbuffer", 0)) if "zoom_levels" not in config: warnings.warn( DeprecationWarning( "use new config element 'zoom_levels' instead of 'process_zoom', " "'process_minzoom' and 'process_maxzoom'" ) ) if "process_zoom" in config: config["zoom_levels"] = config["process_zoom"] elif all([ i in config for i in ["process_minzoom", "process_maxzoom"] ]): config["zoom_levels"] = dict( min=config["process_minzoom"], max=config["process_maxzoom"] ) else: raise MapcheteConfigError("process zoom levels not provided in config") if "bounds" not in config: if "process_bounds" in config: warnings.warn( DeprecationWarning( "'process_bounds' are deprecated and renamed to 'bounds'" ) ) config["bounds"] = config["process_bounds"] else: config["bounds"] = None if "input" not in config: if "input_files" in config: warnings.warn( DeprecationWarning("'input_files' are deprecated and renamed to 'input'") ) config["input"] = config["input_files"] else: raise MapcheteConfigError("no 'input' found") elif "input_files" in config: raise MapcheteConfigError( "'input' and 'input_files' are not allowed at the same time") if "process_file" in config: warnings.warn( DeprecationWarning("'process_file' is deprecated and renamed to 'process'") ) config["process"] = config.pop("process_file") return config