From 2db876ea2e64abd7e22823927717be28ecd9dfee Mon Sep 17 00:00:00 2001 From: Itz-Agasta Date: Thu, 6 Aug 2026 14:31:06 +0530 Subject: [PATCH] Stop creating the place_classtype tables from special phrases The special phrase import created one table per class/type pair, indexed it on centroid and place_id and granted the webuser access to it. Nothing reads those tables anymore, so all of that goes. What is left is collecting the phrases and handing them to the tokenizer. The --min option only decided which of the tables were created, it never filtered the phrases themselves, so it has no meaning without them. It is removed together with get_classtype_pairs(). The table counters in the statistics handler would report zero forever. --- src/nominatim_db/clicmd/args.py | 1 - src/nominatim_db/clicmd/special_phrases.py | 5 +- .../special_phrases/importer_statistics.py | 27 --- .../tools/special_phrases/sp_importer.py | 197 +--------------- .../tools/test_import_special_phrases.py | 220 +----------------- test/python/tools/test_sp_importer.py | 70 ------ 6 files changed, 12 insertions(+), 508 deletions(-) delete mode 100644 test/python/tools/test_sp_importer.py diff --git a/src/nominatim_db/clicmd/args.py b/src/nominatim_db/clicmd/args.py index 663699c0..c37b5f18 100644 --- a/src/nominatim_db/clicmd/args.py +++ b/src/nominatim_db/clicmd/args.py @@ -138,7 +138,6 @@ class NominatimArgs: import_from_wiki: bool import_from_csv: Optional[str] no_replace: bool - min: int # Arguments to all query functions format: str diff --git a/src/nominatim_db/clicmd/special_phrases.py b/src/nominatim_db/clicmd/special_phrases.py index 90560fb7..ef06de7f 100644 --- a/src/nominatim_db/clicmd/special_phrases.py +++ b/src/nominatim_db/clicmd/special_phrases.py @@ -58,8 +58,6 @@ class ImportSpecialPhrases: help='Import special phrases from a CSV file') group.add_argument('--no-replace', action='store_true', help='Keep the old phrases and only add the new ones') - group.add_argument('--min', type=int, default=0, - help='Restrict special phrases by minimum occurance') def run(self, args: NominatimArgs) -> int: @@ -84,9 +82,8 @@ class ImportSpecialPhrases: tokenizer = tokenizer_factory.get_tokenizer_for_db(args.config) should_replace = not args.no_replace - min = args.min with connect(args.config.get_libpq_dsn()) as db_connection: SPImporter( args.config, db_connection, loader - ).import_phrases(tokenizer, should_replace, min) + ).import_phrases(tokenizer, should_replace) diff --git a/src/nominatim_db/tools/special_phrases/importer_statistics.py b/src/nominatim_db/tools/special_phrases/importer_statistics.py index e4271515..e78b1a84 100644 --- a/src/nominatim_db/tools/special_phrases/importer_statistics.py +++ b/src/nominatim_db/tools/special_phrases/importer_statistics.py @@ -25,9 +25,6 @@ class SpecialPhrasesImporterStatistics(): Set all counts for the global import to 0. """ - self.tables_created = 0 - self.tables_deleted = 0 - self.tables_ignored = 0 self.invalids = 0 def notify_one_phrase_invalid(self) -> None: @@ -37,24 +34,6 @@ class SpecialPhrasesImporterStatistics(): """ self.invalids += 1 - def notify_one_table_created(self) -> None: - """ - Add +1 to the count of created tables. - """ - self.tables_created += 1 - - def notify_one_table_deleted(self) -> None: - """ - Add +1 to the count of deleted tables. - """ - self.tables_deleted += 1 - - def notify_one_table_ignored(self) -> None: - """ - Add +1 to the count of ignored tables. - """ - self.tables_ignored += 1 - def notify_import_done(self) -> None: """ Print stats for the whole import process @@ -65,12 +44,6 @@ class SpecialPhrasesImporterStatistics(): LOG.info('- %s phrases were invalid.', self.invalids) if self.invalids > 0: LOG.info(' Those invalid phrases have been skipped.') - LOG.info('- %s tables were ignored as they already exist on the database', - self.tables_ignored) - LOG.info('- %s tables were created', self.tables_created) - LOG.info('- %s tables were deleted from the database', self.tables_deleted) - if self.tables_deleted > 0: - LOG.info(' They were deleted as they are not valid anymore.') if self.invalids > 0: LOG.warning('%s phrases were invalid and have been skipped during the whole process.', diff --git a/src/nominatim_db/tools/special_phrases/sp_importer.py b/src/nominatim_db/tools/special_phrases/sp_importer.py index 12e695b6..288d8d3f 100644 --- a/src/nominatim_db/tools/special_phrases/sp_importer.py +++ b/src/nominatim_db/tools/special_phrases/sp_importer.py @@ -13,14 +13,13 @@ The phrases already present in the database which are not valids anymore are removed. """ -from typing import Iterable, Tuple, Mapping, Sequence, Optional, Set +from typing import Iterable, Tuple, Mapping, Sequence, Set import logging import re -from psycopg.sql import Identifier, SQL from ...typing import Protocol from ...config import Configuration -from ...db.connection import Connection, drop_tables, index_exists +from ...db.connection import Connection from .importer_statistics import SpecialPhrasesImporterStatistics from .special_phrase import SpecialPhrase from ...tokenizer.base import AbstractTokenizer @@ -28,12 +27,6 @@ from ...tokenizer.base import AbstractTokenizer LOG = logging.getLogger() -def _classtype_table(phrase_class: str, phrase_type: str) -> str: - """ Return the name of the table for the given class and type. - """ - return f'place_classtype_{phrase_class}_{phrase_type}' - - class SpecialPhraseLoader(Protocol): """ Protocol for classes implementing a loader for special phrases. """ @@ -60,36 +53,8 @@ class SPImporter(): # This set will contain all existing phrases to be added. # It contains tuples with the following format: (label, class, type, operator) self.word_phrases: Set[Tuple[str, str, str, str]] = set() - # This set will contain all existing place_classtype tables which doesn't match any - # special phrases class/type on the wiki. - self.table_phrases_to_delete: Set[str] = set() - - def get_classtype_pairs(self, min: int = 0) -> Set[Tuple[str, str]]: - """ - Returns list of allowed special phrases from the database, - restricting to a list of combinations of classes and types - which occur equal to or more than a specified amount of times. - - Default value for this is 0, which allows everything in database. - """ - db_combinations = set() - - query = f""" - SELECT class AS CLS, type AS typ - FROM placex - GROUP BY class, type - HAVING COUNT(*) >= {min} - """ - with self.db_connection.cursor() as db_cursor: - db_cursor.execute(SQL(query)) - for row in db_cursor: - db_combinations.add((row[0], row[1])) - - return db_combinations - - def import_phrases(self, tokenizer: AbstractTokenizer, should_replace: bool, - min: int = 0) -> None: + def import_phrases(self, tokenizer: AbstractTokenizer, should_replace: bool) -> None: """ Iterate through all SpecialPhrases extracted from the loader and import them into the database. @@ -99,19 +64,9 @@ class SPImporter(): in the database will be removed. """ LOG.warning('Special phrases importation starting') - self._fetch_existing_place_classtype_tables() - - # Store pairs of class/type for further processing - class_type_pairs = set() for phrase in self.sp_loader.generate_phrases(): - result = self._process_phrase(phrase) - if result: - class_type_pairs.add(result) - - self._create_classtype_table_and_indexes(class_type_pairs, min) - if should_replace: - self._remove_non_existent_tables_from_db() + self._process_phrase(phrase) self.db_connection.commit() @@ -121,22 +76,6 @@ class SPImporter(): LOG.warning('Import done.') self.statistics_handler.notify_import_done() - def _fetch_existing_place_classtype_tables(self) -> None: - """ - Fetch existing place_classtype tables. - Fill the table_phrases_to_delete set of the class. - """ - query = """ - SELECT table_name - FROM information_schema.tables - WHERE table_schema='public' - AND table_name like 'place_classtype_%'; - """ - with self.db_connection.cursor() as db_cursor: - db_cursor.execute(SQL(query)) - for row in db_cursor: - self.table_phrases_to_delete.add(row[0]) - def _load_white_and_black_lists(self) \ -> Tuple[Mapping[str, Sequence[str]], Mapping[str, Sequence[str]]]: """ @@ -160,144 +99,26 @@ class SPImporter(): return False return True - def _process_phrase(self, phrase: SpecialPhrase) -> Optional[Tuple[str, str]]: + def _process_phrase(self, phrase: SpecialPhrase) -> None: """ Processes the given phrase by checking black and white list - and sanity. - Return the class/type pair corresponding to the phrase. + and sanity, and adds it to the phrases to import. """ # blacklisting: disallow certain class/type combinations if phrase.p_class in self.black_list.keys() \ and phrase.p_type in self.black_list[phrase.p_class]: - return None + return # whitelisting: if class is in whitelist, allow only tags in the list if phrase.p_class in self.white_list.keys() \ and phrase.p_type not in self.white_list[phrase.p_class]: - return None + return # sanity check, in case somebody added garbage in the wiki if not self._check_sanity(phrase): self.statistics_handler.notify_one_phrase_invalid() - return None + return self.word_phrases.add((phrase.p_label, phrase.p_class, phrase.p_type, phrase.p_operator)) - - return (phrase.p_class, phrase.p_type) - - def _create_classtype_table_and_indexes(self, - class_type_pairs: Iterable[Tuple[str, str]], - min: int = 0) -> None: - """ - Create table place_classtype for each given pair. - Also create indexes on place_id and centroid. - """ - LOG.warning('Create tables and indexes...') - - sql_tablespace = self.config.TABLESPACE_AUX_DATA - if sql_tablespace: - sql_tablespace = ' TABLESPACE ' + sql_tablespace - - with self.db_connection.cursor() as db_cursor: - db_cursor.execute("CREATE INDEX idx_placex_classtype ON placex (class, type)") - - if min: - allowed_special_phrases = self.get_classtype_pairs(min) - - for pair in class_type_pairs: - phrase_class = pair[0] - phrase_type = pair[1] - - # Will only filter if min is not 0 - if min and (phrase_class, phrase_type) not in allowed_special_phrases: - LOG.warning("Skipping phrase %s=%s: not in allowed special phrases", - phrase_class, phrase_type) - continue - - table_name = _classtype_table(phrase_class, phrase_type) - - if table_name in self.table_phrases_to_delete: - self.statistics_handler.notify_one_table_ignored() - # Remove this table from the ones to delete as it match a - # class/type still existing on the special phrases of the wiki. - self.table_phrases_to_delete.remove(table_name) - # So don't need to create the table and indexes. - continue - - # Table creation - self._create_place_classtype_table(sql_tablespace, phrase_class, phrase_type) - - # Indexes creation - self._create_place_classtype_indexes(sql_tablespace, phrase_class, phrase_type) - - # Grant access on read to the web user. - self._grant_access_to_webuser(phrase_class, phrase_type) - - self.statistics_handler.notify_one_table_created() - - with self.db_connection.cursor() as db_cursor: - db_cursor.execute("DROP INDEX idx_placex_classtype") - - def _create_place_classtype_table(self, sql_tablespace: str, - phrase_class: str, phrase_type: str) -> None: - """ - Create table place_classtype of the given phrase_class/phrase_type - if doesn't exit. - """ - table_name = _classtype_table(phrase_class, phrase_type) - with self.db_connection.cursor() as cur: - cur.execute(SQL("""CREATE TABLE IF NOT EXISTS {} {} AS - SELECT place_id AS place_id, - st_centroid(geometry) AS centroid - FROM placex - WHERE class = %s AND type = %s - """).format(Identifier(table_name), SQL(sql_tablespace)), - (phrase_class, phrase_type)) - - def _create_place_classtype_indexes(self, sql_tablespace: str, - phrase_class: str, phrase_type: str) -> None: - """ - Create indexes on centroid and place_id for the place_classtype table. - """ - index_prefix = f'idx_place_classtype_{phrase_class}_{phrase_type}_' - base_table = _classtype_table(phrase_class, phrase_type) - # Index on centroid - if not index_exists(self.db_connection, index_prefix + 'centroid'): - with self.db_connection.cursor() as db_cursor: - db_cursor.execute(SQL("CREATE INDEX {} ON {} USING GIST (centroid) {}") - .format(Identifier(index_prefix + 'centroid'), - Identifier(base_table), - SQL(sql_tablespace))) - - # Index on place_id - if not index_exists(self.db_connection, index_prefix + 'place_id'): - with self.db_connection.cursor() as db_cursor: - db_cursor.execute(SQL("CREATE INDEX {} ON {} USING btree(place_id) {}") - .format(Identifier(index_prefix + 'place_id'), - Identifier(base_table), - SQL(sql_tablespace))) - - def _grant_access_to_webuser(self, phrase_class: str, phrase_type: str) -> None: - """ - Grant access on read to the table place_classtype for the webuser. - """ - table_name = _classtype_table(phrase_class, phrase_type) - with self.db_connection.cursor() as db_cursor: - db_cursor.execute(SQL("""GRANT SELECT ON {} TO {}""") - .format(Identifier(table_name), - Identifier(self.config.DATABASE_WEBUSER))) - - def _remove_non_existent_tables_from_db(self) -> None: - """ - Remove special phrases which doesn't exist on the wiki anymore. - Delete the place_classtype tables. - """ - LOG.warning('Cleaning database...') - - # Delete place_classtype tables corresponding to class/type which - # are not on the wiki anymore. - drop_tables(self.db_connection, *self.table_phrases_to_delete) - for _ in self.table_phrases_to_delete: - self.statistics_handler.notify_one_table_deleted() diff --git a/test/python/tools/test_import_special_phrases.py b/test/python/tools/test_import_special_phrases.py index acacc480..02fa44d1 100644 --- a/test/python/tools/test_import_special_phrases.py +++ b/test/python/tools/test_import_special_phrases.py @@ -33,24 +33,6 @@ def xml_wiki_content(src_dir): return xml_test_content.read_text(encoding='utf-8') -@pytest.fixture -def default_phrases(table_factory): - table_factory('place_classtype_testclasstypetable_to_delete') - table_factory('place_classtype_testclasstypetable_to_keep') - - -def test_fetch_existing_place_classtype_tables(sp_importer, table_factory): - """ - Check for the fetch_existing_place_classtype_tables() method. - It should return the table just created. - """ - table_factory('place_classtype_testclasstypetable') - - sp_importer._fetch_existing_place_classtype_tables() - contained_table = sp_importer.table_phrases_to_delete.pop() - assert contained_table == 'place_classtype_testclasstypetable' - - def test_check_sanity_class(sp_importer): """ Check for _check_sanity() method. @@ -74,216 +56,18 @@ def test_load_white_and_black_lists(sp_importer): assert isinstance(black_list, dict) and isinstance(white_list, dict) -def test_create_place_classtype_indexes(temp_db_with_extensions, - temp_db_conn, temp_db_cursor, - table_factory, sp_importer): - """ - Test that _create_place_classtype_indexes() create the - place_id index and centroid index on the right place_class_type table. - """ - phrase_class = 'class' - phrase_type = 'type' - table_name = 'place_classtype_{}_{}'.format(phrase_class, phrase_type) - - table_factory(table_name, 'place_id BIGINT, centroid GEOMETRY') - - sp_importer._create_place_classtype_indexes('', phrase_class, phrase_type) - temp_db_conn.commit() - - assert check_placeid_and_centroid_indexes(temp_db_cursor, phrase_class, phrase_type) - - -def test_create_place_classtype_table(temp_db_conn, temp_db_cursor, placex_table, sp_importer): - """ - Test that _create_place_classtype_table() create - the right place_classtype table. - """ - phrase_class = 'class' - phrase_type = 'type' - sp_importer._create_place_classtype_table('', phrase_class, phrase_type) - temp_db_conn.commit() - - assert check_table_exist(temp_db_cursor, phrase_class, phrase_type) - - -def test_grant_access_to_web_user(temp_db_conn, temp_db_cursor, table_factory, - def_config, sp_importer): - """ - Test that _grant_access_to_webuser() give - right access to the web user. - """ - phrase_class = 'class' - phrase_type = 'type' - table_name = 'place_classtype_{}_{}'.format(phrase_class, phrase_type) - - table_factory(table_name) - - sp_importer._grant_access_to_webuser(phrase_class, phrase_type) - temp_db_conn.commit() - - assert check_grant_access(temp_db_cursor, def_config.DATABASE_WEBUSER, - phrase_class, phrase_type) - - -def test_create_place_classtype_table_and_indexes(temp_db_cursor, def_config, placex_row, - sp_importer, temp_db_conn, monkeypatch): - """ - Test that _create_place_classtype_table_and_indexes() - create the right place_classtype tables and place_id indexes - and centroid indexes and grant access to the web user - for the given set of pairs. - """ - pairs = set([('class1', 'type1'), ('class2', 'type2')]) - for pair in pairs: - placex_row(cls=pair[0], typ=pair[1]) # adding to db - sp_importer._create_classtype_table_and_indexes(pairs) - temp_db_conn.commit() - - for pair in pairs: - assert check_table_exist(temp_db_cursor, pair[0], pair[1]) - assert check_placeid_and_centroid_indexes(temp_db_cursor, pair[0], pair[1]) - assert check_grant_access(temp_db_cursor, def_config.DATABASE_WEBUSER, pair[0], pair[1]) - - -def test_remove_non_existent_tables_from_db(sp_importer, default_phrases, - temp_db_conn, temp_db_cursor): - """ - Check for the remove_non_existent_phrases_from_db() method. - - It should removed entries from the word table which are contained - in the words_phrases_to_delete set and not those also contained - in the words_phrases_still_exist set. - - place_classtype tables contained in table_phrases_to_delete should - be deleted. - """ - sp_importer.table_phrases_to_delete = { - 'place_classtype_testclasstypetable_to_delete' - } - - query_tables = """ - SELECT table_name - FROM information_schema.tables - WHERE table_schema='public' - AND table_name like 'place_classtype_%'; - """ - - sp_importer._remove_non_existent_tables_from_db() - temp_db_conn.commit() - - assert temp_db_cursor.row_set(query_tables) \ - == {('place_classtype_testclasstypetable_to_keep', )} - - @pytest.mark.parametrize("should_replace", [(True), (False)]) -def test_import_phrases(monkeypatch, temp_db_cursor, def_config, sp_importer, - placex_row, table_factory, tokenizer_mock, +def test_import_phrases(monkeypatch, sp_importer, tokenizer_mock, xml_wiki_content, should_replace): """ Check that the main import_phrases() method is well executed. - It should create the place_classtype table, the place_id and centroid indexes, - grand access to the web user and executing the SQL functions for amenities. - It should also update the database well by deleting or preserving existing entries - of the database. + It should pass all phrases of the wiki content on to the tokenizer. """ - # Add some data to the database before execution in order to test - # what is deleted and what is preserved. - table_factory('place_classtype_amenity_animal_shelter') - table_factory('place_classtype_wrongclass_wrongtype') - monkeypatch.setattr('nominatim_db.tools.special_phrases.sp_wiki_loader._get_wiki_content', lambda lang: xml_wiki_content) - class_test = 'aerialway' - type_test = 'zip_line' - tokenizer = tokenizer_mock() - placex_row(cls=class_test, typ=type_test) # in db for special phrase filtering - placex_row(cls='amenity', typ='animal_shelter') # in db for special phrase filtering sp_importer.import_phrases(tokenizer, should_replace) assert len(tokenizer.analyser_cache['special_phrases']) == 19 - - assert check_table_exist(temp_db_cursor, class_test, type_test) - assert check_placeid_and_centroid_indexes(temp_db_cursor, class_test, type_test) - assert check_grant_access(temp_db_cursor, def_config.DATABASE_WEBUSER, class_test, type_test) - assert check_table_exist(temp_db_cursor, 'amenity', 'animal_shelter') - if should_replace: - assert not check_table_exist(temp_db_cursor, 'wrong_class', 'wrong_type') - - assert temp_db_cursor.table_exists('place_classtype_amenity_animal_shelter') - if should_replace: - assert not temp_db_cursor.table_exists('place_classtype_wrongclass_wrongtype') - - -def check_table_exist(temp_db_cursor, phrase_class, phrase_type): - """ - Verify that the place_classtype table exists for the given - phrase_class and phrase_type. - """ - return temp_db_cursor.table_exists('place_classtype_{}_{}'.format(phrase_class, phrase_type)) - - -def check_grant_access(temp_db_cursor, user, phrase_class, phrase_type): - """ - Check that the web user has been granted right access to the - place_classtype table of the given phrase_class and phrase_type. - """ - table_name = 'place_classtype_{}_{}'.format(phrase_class, phrase_type) - - temp_db_cursor.execute(""" - SELECT * FROM information_schema.role_table_grants - WHERE table_name='{}' - AND grantee='{}' - AND privilege_type='SELECT'""".format(table_name, user)) - return temp_db_cursor.fetchone() - - -def check_placeid_and_centroid_indexes(temp_db_cursor, phrase_class, phrase_type): - """ - Check that the place_id index and centroid index exist for the - place_classtype table of the given phrase_class and phrase_type. - """ - table_name = 'place_classtype_{}_{}'.format(phrase_class, phrase_type) - index_prefix = 'idx_place_classtype_{}_{}_'.format(phrase_class, phrase_type) - - return ( - temp_db_cursor.index_exists(table_name, index_prefix + 'centroid') - and - temp_db_cursor.index_exists(table_name, index_prefix + 'place_id') - ) - - -@pytest.mark.parametrize("should_replace", [(True), (False)]) -def test_import_phrases_special_phrase_filtering(monkeypatch, temp_db_cursor, def_config, - sp_importer, placex_row, tokenizer_mock, - xml_wiki_content, should_replace): - - monkeypatch.setattr('nominatim_db.tools.special_phrases.sp_wiki_loader._get_wiki_content', - lambda lang: xml_wiki_content) - - class_test = 'aerialway' - type_test = 'zip_line' - - placex_row(cls=class_test, typ=type_test) # add to the database to make valid - tokenizer = tokenizer_mock() - sp_importer.import_phrases(tokenizer, should_replace) - assert ('Zip Line', 'aerialway', 'zip_line', '-') in sp_importer.word_phrases - assert check_table_exist(temp_db_cursor, class_test, type_test) - assert check_placeid_and_centroid_indexes(temp_db_cursor, class_test, type_test) - assert check_grant_access(temp_db_cursor, def_config.DATABASE_WEBUSER, class_test, type_test) - - -def test_get_classtype_pairs_directly(placex_row, temp_db_conn, sp_importer): - for _ in range(101): - placex_row(cls='highway', typ='residential') - for _ in range(99): - placex_row(cls='amenity', typ='toilet') - - temp_db_conn.commit() - - result = sp_importer.get_classtype_pairs(100) - print("RESULT:", result) - assert ('highway', 'residential') in result - assert ('amenity', 'toilet') not in result diff --git a/test/python/tools/test_sp_importer.py b/test/python/tools/test_sp_importer.py deleted file mode 100644 index ba780e0d..00000000 --- a/test/python/tools/test_sp_importer.py +++ /dev/null @@ -1,70 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This file is part of Nominatim. (https://nominatim.org) -# -# Copyright (C) 2026 by the Nominatim developer community. -# For a full list of authors see the git log. - -from nominatim_db.tools.special_phrases.sp_importer import SPImporter - - -# Testing Database Class Pair Retrival using Conftest.py and placex -def test_get_classtype_pair_data(placex_row, def_config, temp_db_conn): - for _ in range(100): - placex_row(cls='highway', typ='motorway') # edge case 100 - - for _ in range(99): - placex_row(cls='amenity', typ='prison') # edge case 99 - - for _ in range(150): - placex_row(cls='tourism', typ='hotel') - - importer = SPImporter(config=def_config, conn=temp_db_conn, sp_loader=None) - - result = importer.get_classtype_pairs(min=100) - - assert result == { - ("highway", "motorway"), - ("tourism", "hotel") - } - - -def test_get_classtype_pair_data_more(placex_row, def_config, temp_db_conn): - for _ in range(99): - placex_row(cls='emergency', typ='firehydrant') # edge case 99, not included - - for _ in range(199): - placex_row(cls='amenity', typ='prison') - - for _ in range(3478): - placex_row(cls='tourism', typ='hotel') - - importer = SPImporter(config=def_config, conn=temp_db_conn, sp_loader=None) - - result = importer.get_classtype_pairs(min=100) - - assert result == { - ("amenity", "prison"), - ("tourism", "hotel") - } - - -def test_get_classtype_pair_data_default(placex_row, def_config, temp_db_conn): - for _ in range(1): - placex_row(cls='emergency', typ='firehydrant') - - for _ in range(199): - placex_row(cls='amenity', typ='prison') - - for _ in range(3478): - placex_row(cls='tourism', typ='hotel') - - importer = SPImporter(config=def_config, conn=temp_db_conn, sp_loader=None) - - result = importer.get_classtype_pairs() - - assert result == { - ("amenity", "prison"), - ("tourism", "hotel"), - ("emergency", "firehydrant") - } -- 2.47.3