Skip to content

Commit 3d82905

Browse files
committed
Fix joins and features routes for new JoinManager
1 parent 227b6b3 commit 3d82905

File tree

4 files changed

+54
-43
lines changed

4 files changed

+54
-43
lines changed

pygeoapi/api/__init__.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -629,15 +629,14 @@ def __init__(self, config: dict, openapi: dict) -> Self | None:
629629
self.api_headers = get_api_rules(self.config).response_headers
630630
self.base_url = get_base_url(self.config)
631631
self.prefetcher = UrlPrefetcher()
632-
self.supports_joins = False
632+
self.join_manager = None
633633

634634
setup_logger(self.config['logging'])
635635

636-
joins_init = getattr(all_apis().get('joins', object()), 'init', None)
637-
if callable(joins_init):
638-
# Initialize OGC API - Joins:
639-
# build reference cache of join tables already/still on the server
640-
self.supports_joins = joins_init(config)
636+
join_manager = getattr(all_apis().get('joins', object()), 'get_manager', None) # noqa
637+
if callable(join_manager):
638+
# Init OGC API - Joins manager (or set to None if not configured)
639+
self.join_manager = join_manager(config)
641640

642641
CHARSET[0] = config['server'].get('encoding', 'utf-8')
643642
if config['server'].get('gzip'):
@@ -911,7 +910,7 @@ def conformance(api: API, request: APIRequest) -> Tuple[dict, int, str]:
911910
apis_dict['itemtypes'].CONFORMANCE_CLASSES_FEATURES)
912911
# If it's an OGC API - Features provider and joins is
913912
# supported, we can also add conformance classes for it
914-
if api.supports_joins:
913+
if api.join_manager:
915914
conformance_list.extend(
916915
apis_dict['joins'].CONFORMANCE_CLASSES)
917916
if provider['type'] == 'record':
@@ -1145,7 +1144,7 @@ def describe_collections(api: API, request: APIRequest,
11451144
'href': f'{api.get_collections_url()}/{k}/items?f={F_HTML}' # noqa
11461145
})
11471146

1148-
if api.supports_joins and collection_data_type == 'feature':
1147+
if api.join_manager and collection_data_type == 'feature':
11491148
# Add links to available OGC API - Joins sources, if any
11501149
collection['links'].extend([{
11511150
'type': FORMAT_TYPES[F_JSON],
@@ -1366,7 +1365,7 @@ def describe_collections(api: API, request: APIRequest,
13661365
})
13671366

13681367
if request.format == F_HTML: # render
1369-
fcm['supports_joins'] = api.supports_joins
1368+
fcm['supports_joins'] = api.join_manager is not None
13701369
fcm['base_url'] = api.base_url
13711370
fcm['collections_path'] = api.get_collections_url()
13721371
if dataset is not None:

pygeoapi/api/itemtypes.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
from pygeofilter.parsers.cql2_json import parse as parse_cql2_json
4848
from pyproj.exceptions import CRSError
4949

50-
from pygeoapi import l10n, join_util
50+
from pygeoapi import l10n
5151
from pygeoapi.api import evaluate_limit
5252
from pygeoapi.crs import (DEFAULT_CRS, DEFAULT_STORAGE_CRS,
5353
create_crs_transform_spec, get_supported_crs_list,
@@ -58,7 +58,8 @@
5858
from pygeoapi.plugin import load_plugin, PLUGINS
5959
from pygeoapi.provider.base import (
6060
ProviderGenericError, ProviderTypeError, SchemaType)
61-
61+
from pygeoapi.join.manager import (
62+
JoinManager, JoinSourceMissingError, JoinSourceNotFoundError)
6263
from pygeoapi.util import (filter_providers_by_type, to_json,
6364
filter_dict_by_key_value, str2bool,
6465
get_provider_by_type, render_j2_template)
@@ -571,20 +572,18 @@ def get_collection_items(
571572
if join_id:
572573
# Post-process FeatureCollection and join with CSV data
573574
try:
574-
join_util.perform_join(content, dataset, join_id)
575+
api.join_manager.perform_join(content, dataset, join_id)
575576
except ValueError as e:
576577
LOGGER.error(f'Invalid request parameter: {e}',
577578
exc_info=True)
578579
return api.get_exception(
579580
HTTPStatus.BAD_REQUEST, headers, request.format,
580581
'InvalidParameterValue', str(e))
581-
except KeyError as e:
582-
msg = 'Join source not found'
583-
LOGGER.error(f'Unknown joinId: {e}',
584-
exc_info=True)
582+
except (JoinSourceNotFoundError, JoinSourceMissingError) as e:
583+
LOGGER.error(f'{e}', exc_info=True)
585584
return api.get_exception(
586585
HTTPStatus.NOT_FOUND, headers, request.format,
587-
'NotFound', msg)
586+
'NotFound', str(e))
588587
except Exception as e:
589588
LOGGER.error(f'Failed to perform join: {e}',
590589
exc_info=True)
@@ -999,20 +998,18 @@ def get_collection_item(api: API, request: APIRequest,
999998
if join_id:
1000999
# Post-process Feature and join with CSV data
10011000
try:
1002-
join_util.perform_join(content, dataset, join_id)
1001+
api.join_manager.perform_join(content, dataset, join_id)
10031002
except ValueError as e:
10041003
LOGGER.error(f'Invalid request parameter: {e}',
10051004
exc_info=True)
10061005
return api.get_exception(
10071006
HTTPStatus.BAD_REQUEST, headers, request.format,
10081007
'InvalidParameterValue', str(e))
1009-
except KeyError as e:
1010-
msg = 'Join source not found'
1011-
LOGGER.error(f'Unknown joinId: {e}',
1012-
exc_info=True)
1008+
except (JoinSourceNotFoundError, JoinSourceMissingError) as e:
1009+
LOGGER.error(f'{e}', exc_info=True)
10131010
return api.get_exception(
10141011
HTTPStatus.NOT_FOUND, headers, request.format,
1015-
'NotFound', msg)
1012+
'NotFound', str(e))
10161013
except Exception as e:
10171014
LOGGER.error(f'Failed to perform join: {e}',
10181015
exc_info=True)
@@ -1104,7 +1101,7 @@ def get_oas_30(cfg: dict, locale: str) -> tuple[list[dict[str, str]], dict[str,
11041101
from pygeoapi.openapi import OPENAPI_YAML, get_visible_collections
11051102

11061103
# We should add a joinId query parameter if OGC API - Joins is enabled
1107-
joins_enabled = join_util.enabled(cfg)
1104+
joins_enabled = JoinManager.configured(cfg)
11081105

11091106
join_id_param = {
11101107
'name': 'joinId',
@@ -1415,9 +1412,13 @@ def get_oas_30(cfg: dict, locale: str) -> tuple[list[dict[str, str]], dict[str,
14151412
}
14161413
}
14171414

1415+
if joins_enabled:
1416+
# Inject joinId parameter into GET /items/{featureId} operation
1417+
paths[f'{items_path}/{{featureId}}']['get']['parameters'].append(join_id_param) # noqa
1418+
14181419
try:
14191420
schema_ref = p.get_schema()
1420-
paths[f'{collection_name_path}/items/{{featureId}}']['get']['responses']['200'] = { # noqa
1421+
paths[f'{items_path}/{{featureId}}']['get']['responses']['200'] = { # noqa
14211422
'content': {
14221423
schema_ref[0]: {
14231424
'schema': schema_ref[1]

pygeoapi/api/joins.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,16 @@
7373
]
7474

7575

76-
def init(cfg: dict) -> bool:
76+
def get_manager(cfg: dict) -> JoinManager | None:
7777
"""
78-
Shortcut to initialize join utility with config.
78+
Shortcut to initialize a JoinManager instance.
7979
Called dynamically by the main `API.__init__` method.
8080
8181
:param cfg: pygeoapi configuration dict
8282
83-
:returns: True if OGC API - Joins is available and initialized
83+
:returns: `JoinManager` instance or None (if OGC API - Joins is disabled)
8484
"""
85-
return join_util.init(cfg)
85+
return JoinManager.from_config(cfg)
8686

8787

8888
def get_oas_30(cfg: dict, locale: str) -> tuple[list[dict[str, str]], dict[str, dict]]: # noqa
@@ -97,7 +97,7 @@ def get_oas_30(cfg: dict, locale: str) -> tuple[list[dict[str, str]], dict[str,
9797

9898
paths = {}
9999

100-
if not join_util.enabled(cfg):
100+
if not get_manager(cfg):
101101
LOGGER.info('OpenAPI: skipping OGC API - Joins endpoints setup')
102102
return [], {'paths': paths}
103103

@@ -540,7 +540,7 @@ def list_joins(api: API, request: APIRequest,
540540
return _not_found(api, request, headers, msg)
541541

542542
try:
543-
sources = join_util.list_sources(dataset)
543+
sources = api.join_manager.list_sources(dataset)
544544
except Exception as e:
545545
LOGGER.error(str(e), exc_info=True)
546546
return _server_error(api, request, headers, str(e))
@@ -645,7 +645,7 @@ def join_details(api: API, request: APIRequest,
645645
return _not_found(api, request, headers, msg)
646646

647647
try:
648-
details = join_util.read_join_source(dataset, join_id)
648+
details = api.join_manager.read_join_source(dataset, join_id)
649649

650650
uri = f'{api.get_collections_url()}/{dataset}'
651651
output = {
@@ -697,10 +697,9 @@ def join_details(api: API, request: APIRequest,
697697
except ValueError as e:
698698
LOGGER.error(f'Invalid request parameter: {e}', exc_info=True)
699699
return _bad_request(api, request, headers, str(e))
700-
except KeyError as e:
701-
msg = 'Collection or join source not found'
702-
LOGGER.error(f'Invalid parameter value: {e}', exc_info=True)
703-
return _not_found(api, request, headers, msg)
700+
except (JoinSourceNotFoundError, JoinSourceMissingError) as e:
701+
LOGGER.error(f'{e}', exc_info=True)
702+
return _not_found(api, request, headers, str(e))
704703
except Exception as e:
705704
LOGGER.error(f'Failed to retrieve join: {e}', exc_info=True)
706705
msg = f'Failed to retrieve join: {str(e)}'
@@ -739,7 +738,7 @@ def create_join(api: API, request: APIRequest,
739738
"""
740739
headers, collections, dataset = _prepare(api, request, collection)
741740

742-
if not api.supports_joins:
741+
if not api.join_manager:
743742
# TODO: perhaps a 406 Not Acceptable would be better?
744743
msg = 'OGC API - Joins is not available on this instance'
745744
return _server_error(api, request, headers, msg)
@@ -768,7 +767,7 @@ def create_join(api: API, request: APIRequest,
768767
# Get provider locale (if any)
769768
prv_locale = l10n.get_plugin_locale(provider_def, request.raw_locale)
770769

771-
details = join_util.process_csv(dataset, provider, request.form)
770+
details = api.join_manager.process_csv(dataset, provider, request.form)
772771

773772
uri = f'{api.get_collections_url()}/{dataset}'
774773
join_id = details['id']
@@ -865,13 +864,14 @@ def delete_join(api: API, request: APIRequest,
865864
return _not_found(api, request, headers, msg)
866865

867866
try:
868-
if not join_util.remove_source(dataset, join_id):
867+
if not api.join_manager.remove_source(dataset, join_id):
869868
msg = f'Join source {join_id} not found for collection {dataset}'
870869
return _not_found(api, request, headers, msg)
871870
except ValueError as e:
872871
LOGGER.error(f'Invalid request parameter: {e}', exc_info=True)
873872
return _bad_request(api, request, headers, str(e))
874873
except Exception as e:
874+
# e.g. unable to delete file from disk
875875
LOGGER.error(f'Failed to delete join source: {e}',
876876
exc_info=True)
877877
msg = f'Failed to delete join: {str(e)}'

pygeoapi/join/manager.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ def __init__(self, source_dir: Path, **kwargs):
7878
self._cleanup_sources()
7979

8080
LOGGER.debug(
81-
f'JoinManager initialized with source_dir: {self._source_dir}')
81+
f'{self.__class__.__name__} initialized with source_dir: '
82+
f'{self._source_dir}')
8283

8384
@property
8485
def source_dir(self) -> Path:
@@ -95,6 +96,16 @@ def max_files(self) -> int:
9596
"""Maximum number of join source files to keep at any time."""
9697
return self._max_files
9798

99+
@staticmethod
100+
def configured(config: dict) -> bool:
101+
"""Quick check to see if OGC API - Joins section is present in the
102+
pygeoapi config dict, without actually initializing a JoinManager."""
103+
try:
104+
return 'joins' in config.get('server', {})
105+
except KeyError:
106+
# pygeoapi was configured without OGC API - Joins
107+
return False
108+
98109
@classmethod
99110
def from_config(cls, config: dict) -> Optional['JoinManager']:
100111
"""
@@ -108,6 +119,7 @@ def from_config(cls, config: dict) -> Optional['JoinManager']:
108119
joins_config = config.get('server', {})['joins']
109120
except KeyError:
110121
# pygeoapi was configured without OGC API - Joins:
122+
LOGGER.debug('pygeoapi server.joins not configured')
111123
return None
112124

113125
# Check if 'joins' key was set, but without further configuration
@@ -156,10 +168,9 @@ def from_config(cls, config: dict) -> Optional['JoinManager']:
156168
# Create and return manager
157169
try:
158170
manager = cls(source_dir, max_days=max_days, max_files=max_files)
159-
LOGGER.debug('JoinManager successfully created')
160171
return manager
161172
except Exception as e:
162-
LOGGER.error(f'Failed to create JoinManager: {e}')
173+
LOGGER.error(f'Failed to initialize {cls.__name__}: {e}')
163174
return None
164175

165176
@contextmanager
@@ -649,7 +660,7 @@ def remove_source(self, collection_id: str, join_id: str) -> bool:
649660
# Source file was removed, but reference still existed: 200
650661
return True
651662

652-
# Remove the JSON file from disk
663+
# Remove the JSON file from disk (note: this is noisy)
653664
deleted = self._delete_source(source_path)
654665

655666
# Remove reference (clean up orphan)

0 commit comments

Comments
 (0)