From 5854e0f45a07cb28f94eee89266d2e822f56950f Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 13 Aug 2024 09:52:18 -0400 Subject: [PATCH] Code for official PR to add hot-reload logic mechanisms --- pygeoapi/api/__init__.py | 94 ++++++++++++++++++++++++++++++++++++++- pygeoapi/api/coverages.py | 3 +- pygeoapi/api/itemtypes.py | 7 ++- 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/pygeoapi/api/__init__.py b/pygeoapi/api/__init__.py index b47541f231..283d0803a7 100644 --- a/pygeoapi/api/__init__.py +++ b/pygeoapi/api/__init__.py @@ -43,7 +43,7 @@ import asyncio from collections import OrderedDict from copy import deepcopy -from datetime import datetime +from datetime import (datetime, timezone) from functools import partial from gzip import compress from http import HTTPStatus @@ -54,6 +54,7 @@ from dateutil.parser import parse as dateparse import pytz +import yaml from pygeoapi import __version__, l10n from pygeoapi.linked_data import jsonldify, jsonldify_collection @@ -220,6 +221,35 @@ def apply_gzip(headers: dict, content: Union[str, bytes]) -> Union[str, bytes]: return content +def pre_load_colls(func): + """ + Decorator that makes sure the loaded collections in memory are update to + date. + + :param func: decorated function + + :returns: `func` + """ + + def inner(*args, **kwargs): + cls = args[0] + + if hasattr(cls, 'reload_resources_if_necessary'): + # Validate the resources are up to date + cls.reload_resources_if_necessary() + + else: + cls = args[1] + + if hasattr(cls, 'reload_resources_if_necessary'): + # Validate the resources are up to date + cls.reload_resources_if_necessary() + + return func(*args, **kwargs) + + return inner + + class APIRequest: """ Transforms an incoming server-specific Request into an object @@ -682,9 +712,70 @@ def __init__(self, config, openapi): self.tpl_config = deepcopy(self.config) self.tpl_config['server']['url'] = self.base_url + # Now that basic configuration is read, call the load ressources. + # This call enables the api engine to load resources dynamically. + # That is, resources which could be coming from other sources than + # the yaml file itself. Indeed, the yaml file could be empty of + # resources and all read dynamically from somewhere else + # (e.g. a database). + # That way, it's a little easier to manage a dynamic ensemble of + # resoures, especially on pygeoapi distributed environments. + self.load_resources() + self.manager = get_manager(self.config) LOGGER.info('Process manager plugin loaded') + def load_resources(self): + """ + Calls on_load_resources and reassigns the resources configuration. + """ + + # Call on_load_resources sending the current resources configuration. + self.config['resources'] = self.on_load_resources(self.config['resources']) # noqa + + # Copy over for the template config (this is something that got added + # after a rebase of pending PR.. to be investigated..) + self.tpl_config['resources'] = deepcopy(self.config['resources']) + + # Keep track of UTC date of last load + self.last_loaded_resources = datetime.now(timezone.utc) + + def on_load_resources(self, resources): + """ + Overridable function to load (or reload) the available resources + dynamically. + By default, this function simply returns the resources as-is. This is + the original behavior of the API; expecting resources to be + already configured correctly per the yaml config file. + + :param resources: the resources as currently configured + (self.config['resources']) + :returns: the resources dictionary that's available in the API. + """ + + # By default, return the same resources object, unchanged. + return resources + + def on_load_resources_check(self, last_loaded_resources): + """ + Overridable function to check if the resources should be reloaded. + As this implementation depends on your messaging broker, by default, + pygeoapi doesn't support that and returns False. + """ + return False + + def reload_resources_if_necessary(self): + """ + This function reloads the resources if necessary, by calling + 'on_load_resources_check' and then calling 'load_resources' if + necessary. + """ + + # If the resources should be reloaded + if self.on_load_resources_check(self.last_loaded_resources): + # Reload the resources + self.load_resources() + @gzip @pre_process @jsonldify @@ -898,6 +989,7 @@ def conformance(self, @gzip @pre_process @jsonldify + @pre_load_colls def describe_collections(self, request: Union[APIRequest, Any], dataset=None) -> Tuple[dict, int, str]: """ diff --git a/pygeoapi/api/coverages.py b/pygeoapi/api/coverages.py index c6687042c8..0e3fcdb1eb 100644 --- a/pygeoapi/api/coverages.py +++ b/pygeoapi/api/coverages.py @@ -51,7 +51,7 @@ from . import ( APIRequest, API, F_JSON, SYSTEM_LOCALE, validate_bbox, validate_datetime, - validate_subset + validate_subset, pre_load_colls ) LOGGER = logging.getLogger(__name__) @@ -68,6 +68,7 @@ ] +@pre_load_colls def get_collection_coverage( api: API, request: APIRequest, dataset) -> Tuple[dict, int, str]: """ diff --git a/pygeoapi/api/itemtypes.py b/pygeoapi/api/itemtypes.py index 661f5cd80b..91f540f4ad 100644 --- a/pygeoapi/api/itemtypes.py +++ b/pygeoapi/api/itemtypes.py @@ -63,7 +63,7 @@ from . import ( APIRequest, API, SYSTEM_LOCALE, F_JSON, FORMAT_TYPES, F_HTML, F_JSONLD, - validate_bbox, validate_datetime + validate_bbox, validate_datetime, pre_load_colls ) LOGGER = logging.getLogger(__name__) @@ -100,6 +100,7 @@ ] +@pre_load_colls def get_collection_queryables(api: API, request: Union[APIRequest, Any], dataset=None) -> Tuple[dict, int, str]: """ @@ -201,6 +202,7 @@ def get_collection_queryables(api: API, request: Union[APIRequest, Any], return headers, HTTPStatus.OK, to_json(queryables, api.pretty_print) +@pre_load_colls def get_collection_items( api: API, request: Union[APIRequest, Any], dataset) -> Tuple[dict, int, str]: @@ -638,6 +640,7 @@ def get_collection_items( return headers, HTTPStatus.OK, to_json(content, api.pretty_print) +@pre_load_colls def post_collection_items( api: API, request: APIRequest, dataset) -> Tuple[dict, int, str]: """ @@ -923,6 +926,7 @@ def post_collection_items( return headers, HTTPStatus.OK, to_json(content, api.pretty_print) +@pre_load_colls def manage_collection_item( api: API, request: APIRequest, action, dataset, identifier=None) -> Tuple[dict, int, str]: @@ -1034,6 +1038,7 @@ def manage_collection_item( return headers, HTTPStatus.OK, '' +@pre_load_colls def get_collection_item(api: API, request: APIRequest, dataset, identifier) -> Tuple[dict, int, str]: """