configuration.py 28.3 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env python3

# Copyright (C) 2021:
#   Helmholtz-Zentrum Potsdam Deutsches GeoForschungsZentrum GFZ
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
# General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see http://www.gnu.org/licenses/.

import sys
import logging
import mercantile
from copy import deepcopy
from gdeimporter.tools.configuration_methods import ConfigurationMethods
from gdeimporter.exposureentity import ExposureEntity
from gdeexporter.database_queries import DatabaseQueries

logger = logging.getLogger()

29
30
31
32
33
34
# Currently supported output formats
OUTPUT_FORMATS = ["OpenQuake_CSV"]

# Currently supported types of buildings to export
SUPPORTED_BUILDING_TYPES = ["OBM", "remainder", "aggregated"]

35
36
37
38
39
40
41

class Configuration:
    """This class handles the configuration parameters of the gde-exporter.

    Attributes:
        self.model_name (str):
            Name of the input aggregated model.
42
43
        self.output_path (str):
            Path where the export files will be stored.
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
        self.occupancies_to_run (list of str):
            List of occupancy cases of the input aggregated exposure model for which the code
            will be run.
        self.exposure_entities_to_run (list of str):
            List of names of the exposure entities for which the code will be run.
        self.exposure_entities_code (str or dict):
            If "ISO3" (str), the country ISO3 codes associated with the names of the exposure
            entities will be automatically retrieved and used as their codes. Otherwise it needs
            to be a dicionary whose keys are the names of the exposure entities. The content
            within each key is a 3-character string to be used as the code for the corresponding
            exposure entity. E.g.:
            self.exposure_entities_code = {
                "Exposure Entity 1": "EE1",
                "Exposure Entity 2": "XXX"
            }
        self.geographic_selection (dict):
            Dictionary of parameters that define the selection of quadkeys for which the code
            will be run. It contains (some of) the following keys:
                selection_mode (str):
                    Way in which the quadkeys will be selected. Supported values are:
                        "exposure_entity":
                            All quadkeys that exist in the 'data_unit_tiles' table of the GDE
                            Tiles database and that are associated with the exposure entities
                            indicated in self.exposure_entities_to_run and the aggregated
                            exposure model indicated in self.model_name.
                        "data_unit_id":
                            All quadkeys that exist in the 'data_unit_tiles' table of the GDE
                            Tiles database and that are associated with the data unit IDs
                            indicated in self.geographic_selection["data_unit_ids"] and the
                            aggregated exposure model indicated in self.model_name.
                        "quadkeys":
                            All quadkeys retrieved from a TXT file whose path is indicated in
                            self.geographic_selection["quadkeys_file"]. Only data associated
                            with the exposure entities indicated in
                            self.exposure_entities_to_run and the aggregated exposure model
                            indicated in self.model_name will be processed.
                        "bounding_box":
                            All quadkeys of the tiles that contain the bounding box with limits
                            indicated in self.geographic_selection["bounding_box"]. Only data
                            associated with the exposure entities indicated in
                            self.exposure_entities_to_run and the aggregated exposure model
                            indicated in self.model_name will be processed.
                quadkeys_file (str):
                    The path to a TXT file that contains a list of quadkeys to be processed.
                    Only needed if selection_mode = "quadkeys".
                data_unit_ids (list of str):
                    List of data unit IDS to be processed. Only needed if selection_mode =
                    "data_unit_id".
                bounding_box (dict):
                    Dictionary with the coordinates of the bounding box to be processed. Only
                    needed if selection_mode = "bounding_box". The keys are:
                        "lon_w" (float): West-most longitude.
                        "lon_e" (float): East-most longitude.
                        "lat_s" (float): South-most latitude.
                        "lat_n" (float): North-most latitude.
99
100
101
102
103
104
105
106
107
108
109
110
        self.cost_cases (dict):
            Dictionary containing indications on the sort of costs to retrieve. The minimum
            number of keys is one. The sort of costs that are available are: structural,
            non_structural, contents and total. The keys are the names as they will appear in
            the output, the values refer to the intrinsic naming in the model (i.e. the way
            values are stored in the database).
        self.people_cases (dict):
            Dictionary containing indications on the time of the day for which the number of
            people in the buildings is to be output. The minimum number of keys is one. The
            available times of the day are: day, night, transit and census. The keys are the
            names as they will appear in the output, the values refer to the intrinsic naming in
            the model (i.e. the way values are stored in the database).
111
112
113
114
115
116
        self.output_format (list of str):
            Format to which the GDE model will be exported. Currently supported options:
            OpenQuake_CSV.
        self.buildings_to_export (list of str):
            List of types of buildings to export. Currently supported values: OBM, remainder,
            aggregated.
117
118
119
        self.export_OBM_footprints (bool):
            If True, the geometries of OpenBuildingMap buildings will be retrieved and exported,
            if False, they will not.
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
        self.database_gde_tiles (dict):
            Dictionary containing the credentials needed to connect to the SQL database in which
            information on the GDE tiles is stored. The exact parameters needed depend on the
            database. They can be:
                host (str):
                    SQL database host address.
                dbname (str):
                    Name of the SQL database.
                port (int):
                    Port where the SQL database can be found.
                username (str):
                    User name to connect to the SQL database.
                password (str):
                    Password associated with self.username.
        self.quadkeys_to_process (dict):
            Dictionary whose keys contain lists of quadkeys to be processed. The keys depend on
            self.geographic_selection["selection_mode"] and can be:
                - exposure entity codes
                - data unit IDs
                - "quadkeys_list"
                - "bounding_box"
        self.number_quadkeys_to_process (int):
            Total number of quadkeys to process (from all keys of self.quadkeys_to_process).
143
144
        self.number_cores (int):
            Number of cores that will be used to run the code.
145
146
147
148
    """

    REQUIRES = [
        "model_name",
149
        "output_path",
150
151
152
153
        "occupancies_to_run",
        "exposure_entities_to_run",
        "exposure_entities_code",
        "geographic_selection",
154
155
        "cost_cases",
        "people_cases",
156
157
        "output_format",
        "buildings_to_export",
158
        "export_OBM_footprints",
159
        "database_gde_tiles",
160
        "number_cores",
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
    ]

    def __init__(self, filepath, force_config_over_hierarchies=False):
        """
        Args:
            filepath (str):
                Full file path to the .yml configuration file.
            force_config_over_hierarchies (bool):
                If True, the contents of the .yml configuration file specified in filepath will
                take precedence over any other hierarchy (e.g. preference of environment
                variables if they exist). If False, hierarchies of preference established in
                this class are applied. This parameter is used for forcing the testing of this
                class under certain circumstances. Default: False.
        """

        config = ConfigurationMethods.read_config_file(filepath)

        self.model_name = ConfigurationMethods.assign_parameter(config, "model_name")

180
181
        self.output_path = ConfigurationMethods.assign_parameter(config, "output_path")

182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
        self.occupancies_to_run = ConfigurationMethods.assign_listed_parameters(
            config, "occupancies_to_run"
        )

        self.exposure_entities_to_run = ConfigurationMethods.assign_listed_parameters(
            config, "exposure_entities_to_run"
        )

        try:
            self.exposure_entities_code = ConfigurationMethods.validate_exposure_entities_code(
                config
            )
        except ValueError as e:
            error_message = (
                "Error: the configuration file assigns unsupported values "
                "to exposure_entities_code. The program cannot run. %s" % (e)
            )
            logger.critical(error_message)
            sys.exit(1)
        except TypeError as e:
            error_message = (
                "Error: the configuration file assigns an unsupported data type "
                "to exposure_entities_code. The program cannot run. %s" % (e)
            )
            logger.critical(error_message)
            sys.exit(1)

        self.geographic_selection = ConfigurationMethods.assign_hierarchical_parameters(
            config, "geographic_selection", requested_nested=["selection_mode"]
        )
        self.interpret_geographic_selection()

214
215
216
217
218
219
220
221
222
223
        self.cost_cases = ConfigurationMethods.assign_hierarchical_parameters(
            config, "cost_cases"
        )
        self.validate_cost_cases()

        self.people_cases = ConfigurationMethods.assign_hierarchical_parameters(
            config, "people_cases"
        )
        self.validate_people_cases()

224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
        self.output_format = ConfigurationMethods.assign_listed_parameters(
            config, "output_format"
        )
        for output_format in self.output_format:
            if output_format not in OUTPUT_FORMATS:
                error_message = (
                    "Error: output format specified in configuration file is not recognised. "
                    "Currently supported values: %s." % (", ".join(OUTPUT_FORMATS))
                )
                logger.critical(error_message)
                raise OSError(error_message)

        self.buildings_to_export = ConfigurationMethods.assign_listed_parameters(
            config, "buildings_to_export"
        )
        for building_type in self.buildings_to_export:
            if building_type not in SUPPORTED_BUILDING_TYPES:
                error_message = (
                    "Error: building tpye specified in configuration file is not recognised. "
                    "Currently supported values: %s." % (", ".join(SUPPORTED_BUILDING_TYPES))
                )
                logger.critical(error_message)
                raise OSError(error_message)

248
249
250
251
        self.export_OBM_footprints = ConfigurationMethods.assign_boolean_parameter(
            config, "export_OBM_footprints"
        )

252
253
254
255
256
257
258
259
        self.database_gde_tiles = ConfigurationMethods.retrieve_database_credentials(
            config,
            "database_gde_tiles",
            "test_db_gde_tiles.env",
            "GDEEXPORTER",
            force_config_over_hierarchies,
        )

260
261
262
263
        self.number_cores = ConfigurationMethods.assign_integer_parameter(
            config, "number_cores"
        )

264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
        self.quadkeys_to_process = None
        self.number_quadkeys_to_process = None

        # Terminate if critical parameters are missing (not all parameters are critical)
        for key_parameter in self.REQUIRES:
            if getattr(self, key_parameter) is None:
                error_message = (
                    "Error: parameter '%s' could not be retrieved from "
                    "configuration file. The program cannot run." % (key_parameter)
                )
                logger.critical(error_message)
                raise OSError(error_message)

    def interpret_exposure_entities_to_run(self, aggregated_source_id=0):
        """This function interprets the value assigned to self.exposure_entities_to_run from the
        configuration file and updates self.exposure_entities_to_run accordingly.

        Args:
            aggregated_source_id (int):
                ID of the source of the aggregated exposure model to be run. Only needed if
                'exposure_entities_to_run' is "all" in the configuration file.

        Returns:
            The method updates self.exposure_entities_to_run as a function of its content.
            Possibilities:
                self.exposure_entities_to_run == ["all"]:
                    self.exposure_entities_to_run is updated to contain the list of 3-character
                    codes of all exposure entities associated with 'aggregated_source_id' in the
                    database self.database_gde_tiles.
                self.exposure_entities_to_run contains a list with a path to a .txt or .csv
                file:
                    self.exposure_entities_to_run is updated to contain the list of 3-character
                    codes of the exposure entities listed in the indicated .txt/.csv file.
                self.exposure_entities_to_run contains a list with one or more names of exposure
                entities:
                    self.exposure_entities_to_run is updated to contain the list of 3-character
                    codes of these names.
                Any other case:
                    self.exposure_entities_to_run becomes an empty list.
        """

        if self.exposure_entities_to_run[0].lower() == "all":
            # Retrieve 3-char codes of all exposure entities associated with
            # aggregated_source_id in the 'data_units' table of self.database_gde_tiles
            self.exposure_entities_to_run = (
                DatabaseQueries.retrieve_all_exposure_entities_of_aggregated_source_id(
                    aggregated_source_id, self.database_gde_tiles, "data_units"
                )
            )

            return

        if len(self.exposure_entities_to_run) > 0:
            # Keep the original content (several names are listed)
            exposure_entities_full_names = deepcopy(self.exposure_entities_to_run)

        if (
            self.exposure_entities_to_run[0].split(".")[-1] == "txt"
            or self.exposure_entities_to_run[0].split(".")[-1] == "csv"
        ):
            # Retrieve names of exposure entities from the indicated file
            with open(self.exposure_entities_to_run[0], "r") as f:
                exposure_entities_full_names = []
                for row in f.readlines():
                    raw_row = row.rstrip("\n")
                    raw_row = raw_row.split(",")
                    for element in raw_row:
                        exposure_entities_full_names.append(element)
            f.close()

        exposure_entities_to_run = []

        if isinstance(self.exposure_entities_code, str):
            # If so, it's already been validated that it's "ISO3"
            for full_name in exposure_entities_full_names:
                iso3_code = ExposureEntity.retrieve_country_ISO3(full_name)
                if iso3_code is not None:
                    exposure_entities_to_run.append(iso3_code)
                else:
                    logger.warning(
                        "ExposureEntity.retrieve_country_ISO3 has returned 'None' for exposure "
                        "entity %s. %s will not be run." % (full_name, full_name)
                    )

        if isinstance(self.exposure_entities_code, dict):
            for full_name in exposure_entities_full_names:
                if full_name in self.exposure_entities_code.keys():
                    exposure_entities_to_run.append(self.exposure_entities_code[full_name])
                else:
                    logger.warning(
                        "'exposure_entities_code' in the configuration file does not contain a "
                        "code for exposure entity %s. %s will not be run."
                        % (full_name, full_name)
                    )

        self.exposure_entities_to_run = exposure_entities_to_run

        return

    def interpret_geographic_selection(self):
        """
        This function interprets the contents of self.geographic_selection. Different attributes
        are needed depending on the selection mode specified by the user:
            selection_mode = "exposure_entity" requires no specific attributes
            selection_mode = "data_unit_id" requires self.geographic_selection["data_unit_ids"]
            selection_mode = "quadkeys" requires self.geographic_selection["quadkeys_file"]
            selection_mode = "bounding_box" requires self.geographic_selection["bounding_box"]

        An error is raised when these conditions are not satisfied.

        Unnecessary attributes are set to None in self.geographic_selection.
        """

        missing_attr = None

        if self.geographic_selection["selection_mode"].lower() == "exposure_entity":
            self.geographic_selection["data_unit_ids"] = None
            self.geographic_selection["quadkeys_file"] = None
            self.geographic_selection["bounding_box"] = None

        if self.geographic_selection["selection_mode"].lower() == "data_unit_id":
            if "data_unit_ids" not in self.geographic_selection:
                missing_attr = ("data_unit_id", "data_unit_ids")
            else:
                self.geographic_selection[
                    "data_unit_ids"
                ] = ConfigurationMethods.assign_listed_parameters(
                    self.geographic_selection, "data_unit_ids"
                )
            self.geographic_selection["quadkeys_file"] = None
            self.geographic_selection["bounding_box"] = None

        if self.geographic_selection["selection_mode"].lower() == "quadkeys":
            if "quadkeys_file" not in self.geographic_selection:
                missing_attr = ("quadkeys", "quadkeys_file")
            self.geographic_selection["data_unit_ids"] = None
            self.geographic_selection["bounding_box"] = None

        if self.geographic_selection["selection_mode"].lower() == "bounding_box":
            if "bounding_box" not in self.geographic_selection:
                missing_attr = ("bounding_box", "bounding_box")
            self.geographic_selection["quadkeys_file"] = None
            self.geographic_selection["data_unit_ids"] = None

        if missing_attr is not None:
            error_message = (
                "Error: selection mode is '%s' but parameter '%s' is missing from "
                "configuration file. The program cannot run."
                % (missing_attr[0], missing_attr[1])
            )
            logger.critical(error_message)
            raise OSError(error_message)

        return

    def determine_quadkeys_to_process(
        self, aggregated_source_id, db_gde_tiles_config, db_table
    ):
        """
        This function identifies the quadkeys to be processed based on the contents of
        self.geographic_selection:
            selection_mode = "exposure_entity":
                This function will retrieve the quadkeys associated with each of the exposure
                entities especified in self.exposure_entities_to_run and 'aggregated_source_id'
                from the 'db_table' of the database whose credentials are given in
                'db_gde_tiles_config'.
            selection_mode = "data_unit_id":
                This function will retrieve the quadkeys associated with each of the data units
                especified in self.geographic_selection["data_unit_ids"] and
                'aggregated_source_id' from the 'db_table' of the database whose credentials are
                given in 'db_gde_tiles_config'.
            selection_mode = "quadkeys":
                This function will retrieve the quadkeys specified in the TXT file whose file
                path is indicated in self.geographic_selection["quadkeys_file"].
            selection_mode = "bounding_box":
                This function will identify the quadkeys that contain the bounding box defined
                by the coordinates specified in self.geographic_selection["bounding_box"].

        Args:
            aggregated_source_id (int):
                ID of the source of the aggregated exposure model for which the data unit IDs
                and geometries will be retrieved.
            db_gde_tiles_config (dict):
                Dictionary containing the credentials needed to connect to the SQL database in
                which information on the data units is stored. The keys of the dictionary need
                to be:
                    host (str):
                        SQL database host address.
                    dbname (str):
                        Name of the SQL database.
                    port (int):
                        Port where the SQL database can be found.
                    username (str):
                        User name to connect to the SQL database.
                    password (str):
                        Password associated with self.username.
            db_table (str):
                Name of the table of the SQL database where the data units are stored. It is
                assumed that this table contains, at least, the following fields:
                    quadkey (str):
                        String indicating the quadkey of a tile.
                    aggregated_source_id (int):
                        ID of the source of the aggregated exposure model.
                    exposure_entity (str):
                        3-character code of the exposure entity.
                    data_unit_id (str):
                        ID of the data unit.

        Returns:
            This function updates self.quadkeys_to_process and self.number_quadkeys_to_process.
        """

        if self.geographic_selection["selection_mode"].lower() == "exposure_entity":
            quadkeys_to_process = {}
            number_quadkeys = 0
            for exposure_entity_code in self.exposure_entities_to_run:
                quadkeys_list = (
                    DatabaseQueries.retrieve_quadkeys_by_exposure_entity_aggregated_source_id(
                        exposure_entity_code,
                        aggregated_source_id,
                        self.database_gde_tiles,
                        "data_unit_tiles",
                    )
                )
488
489
490
491
492
493
494
495
496

                if len(quadkeys_list) > 0:
                    quadkeys_to_process[exposure_entity_code] = quadkeys_list
                    number_quadkeys += len(quadkeys_list)
                else:
                    logger.info(
                        "No quadkeys found for exposure entity '%s', skipping"
                        % (exposure_entity_code)
                    )
497
498
499
500
501
502
503
504
505
506
507
508
509

        if self.geographic_selection["selection_mode"].lower() == "data_unit_id":
            quadkeys_to_process = {}
            number_quadkeys = 0
            for data_unit_id in self.geographic_selection["data_unit_ids"]:
                quadkeys_list = (
                    DatabaseQueries.retrieve_quadkeys_by_data_unit_id_aggregated_source_id(
                        data_unit_id,
                        aggregated_source_id,
                        self.database_gde_tiles,
                        "data_unit_tiles",
                    )
                )
510
511
512
513
514
515
516
517

                if len(quadkeys_list) > 0:
                    quadkeys_to_process[data_unit_id] = quadkeys_list
                    number_quadkeys += len(quadkeys_list)
                else:
                    logger.info(
                        "No quadkeys found for data unit '%s', skipping" % (data_unit_id)
                    )
518
519
520
521
522
523
524
525
526
527
528
529
530
531

        if self.geographic_selection["selection_mode"].lower() == "quadkeys":
            # Retrieve quadkeys from the indicated file
            quadkeys_list = []
            with open(self.geographic_selection["quadkeys_file"], "r") as f:
                for row in f.readlines():
                    raw_row = row.rstrip("\n")
                    raw_row = raw_row.split(",")
                    for element in raw_row:
                        quadkeys_list.append(element)
            f.close()
            quadkeys_list = list(dict.fromkeys(quadkeys_list))
            number_quadkeys = len(quadkeys_list)

532
533
534
535
536
537
538
539
            if len(quadkeys_list) > 0:
                quadkeys_to_process = {"quadkeys_list": quadkeys_list}
            else:
                logger.info(
                    "No quadkeys found in '%s'" % (self.geographic_selection["quadkeys_file"])
                )
                quadkeys_to_process = {}

540
541
542
543
544
545
546
547
548
549
        if self.geographic_selection["selection_mode"].lower() == "bounding_box":
            tiles = list(
                mercantile.tiles(
                    self.geographic_selection["bounding_box"]["lon_w"],
                    self.geographic_selection["bounding_box"]["lat_s"],
                    self.geographic_selection["bounding_box"]["lon_e"],
                    self.geographic_selection["bounding_box"]["lat_n"],
                    18,
                )
            )
550

551
552
553
            quadkeys_list = list([mercantile.quadkey(tile) for tile in tiles])
            number_quadkeys = len(quadkeys_list)

554
555
556
557
558
559
            if len(quadkeys_list) > 0:
                quadkeys_to_process = {"bounding_box": quadkeys_list}
            else:
                logger.info("No quadkeys found in bounding box")
                quadkeys_to_process = {}

560
561
562
563
        self.quadkeys_to_process = quadkeys_to_process
        self.number_quadkeys_to_process = number_quadkeys

        return
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609

    def validate_cost_cases(self):
        """
        This function guarantees that the cost cases indicated as values of the self.cost_cases
        dictionary are only those supported by this software. Currently supported values are:
        "structural", "non_structural", "contents" and "total". If any other value is found, the
        item is removed from self.cost_cases and a warning is logged.
        """

        valid_cost_cases = ["structural", "non_structural", "contents", "total"]

        to_delete = []
        for cost_case_key in self.cost_cases.keys():
            if self.cost_cases[cost_case_key] not in valid_cost_cases:
                logger.warning(
                    "Invalid cost case found in configuration file: "
                    "cost case '%s':'%s' will be ignored"
                    % (cost_case_key, self.cost_cases[cost_case_key])
                )
                to_delete.append(cost_case_key)

        for case_to_delete in to_delete:
            del self.cost_cases[case_to_delete]

    def validate_people_cases(self):
        """
        This function guarantees that the people cases indicated as values of the
        self.people_cases dictionary are only those supported by this software. Currently
        supported values are: "day", "night", "transit" and "census". If any other value is
        found, the item is removedfrom self.people_cases and a warning is logged.
        """

        valid_people_cases = ["day", "night", "transit", "census", "average"]

        to_delete = []
        for people_case_key in self.people_cases.keys():
            if self.people_cases[people_case_key] not in valid_people_cases:
                logger.warning(
                    "Invalid people case found in configuration file: "
                    "people case '%s':'%s' will be ignored"
                    % (people_case_key, self.people_cases[people_case_key])
                )
                to_delete.append(people_case_key)

        for case_to_delete in to_delete:
            del self.people_cases[case_to_delete]