|
42 | 42 |
|
43 | 43 | from collections import ChainMap, OrderedDict
|
44 | 44 | from copy import deepcopy
|
45 |
| -from datetime import datetime |
| 45 | +from datetime import (datetime, timezone) |
46 | 46 | from functools import partial
|
47 | 47 | from gzip import compress
|
48 | 48 | from http import HTTPStatus
|
@@ -165,6 +165,32 @@ def apply_gzip(headers: dict, content: Union[str, bytes]) -> Union[str, bytes]:
|
165 | 165 | return content
|
166 | 166 |
|
167 | 167 |
|
| 168 | +def pre_load_colls(func): |
| 169 | + """ |
| 170 | + Decorator function that makes sure the loaded collections are updated. |
| 171 | + This is used when the resources are loaded dynamically, not strictly |
| 172 | + from the yaml file. |
| 173 | +
|
| 174 | + :param func: decorated function |
| 175 | +
|
| 176 | + :returns: `func` |
| 177 | + """ |
| 178 | + |
| 179 | + def inner(*args, **kwargs): |
| 180 | + cls = args[0] |
| 181 | + |
| 182 | + # Validation on the method name for the provided class instance on this |
| 183 | + # decoration function |
| 184 | + if hasattr(cls, 'reload_resources_if_necessary'): |
| 185 | + # Validate the resources are up to date |
| 186 | + cls.reload_resources_if_necessary() |
| 187 | + |
| 188 | + # Continue |
| 189 | + return func(*args, **kwargs) |
| 190 | + |
| 191 | + return inner |
| 192 | + |
| 193 | + |
168 | 194 | class APIRequest:
|
169 | 195 | """
|
170 | 196 | Transforms an incoming server-specific Request into an object
|
@@ -564,9 +590,74 @@ def __init__(self, config, openapi):
|
564 | 590 | self.tpl_config = deepcopy(self.config)
|
565 | 591 | self.tpl_config['server']['url'] = self.base_url
|
566 | 592 |
|
| 593 | + # Now that the basic configuration is read, call the load_resources function. # noqa |
| 594 | + # This call enables the api engine to load resources dynamically. |
| 595 | + # This pattern allows for loading resources coming from another |
| 596 | + # source (e.g. a database) rather than from the yaml file. |
| 597 | + # This, along with the @pre_load_colls decorative function, enables |
| 598 | + # resources management on multiple distributed pygeoapi instances. |
| 599 | + self.load_resources() |
| 600 | + |
567 | 601 | self.manager = get_manager(self.config)
|
568 | 602 | LOGGER.info('Process manager plugin loaded')
|
569 | 603 |
|
| 604 | + def on_load_resources(self, resources: dict) -> dict: |
| 605 | + """ |
| 606 | + Overridable function to load the available resources dynamically. |
| 607 | + By default, this function simply returns the provided resources |
| 608 | + as-is. This is the native behavior of the API; expecting |
| 609 | + resources to be configured in the yaml config file. |
| 610 | +
|
| 611 | + :param resources: the resources as currently configured |
| 612 | + (self.config['resources']) |
| 613 | + :returns: the resources dictionary that's available in the API. |
| 614 | + """ |
| 615 | + |
| 616 | + # By default, return the same resources object, unchanged. |
| 617 | + return resources |
| 618 | + |
| 619 | + def on_load_resources_check(self, last_loaded_resources: datetime) -> bool: # noqa |
| 620 | + """ |
| 621 | + Overridable function to check if the resources should be reloaded. |
| 622 | + Return True in your API implementation when resources should be |
| 623 | + reloaded. This implementation depends on your environment and |
| 624 | + messaging broker. |
| 625 | + Natively, the resources used by the pygeoapi instance are strictly |
| 626 | + the ones from the yaml configuration file. It doesn't support |
| 627 | + resources changing on-the-fly. Therefore, False is returned here |
| 628 | + and they are never reloaded. |
| 629 | + """ |
| 630 | + |
| 631 | + # By default, return False to not reload the resources. |
| 632 | + return False |
| 633 | + |
| 634 | + def load_resources(self) -> None: |
| 635 | + """ |
| 636 | + Calls on_load_resources and reassigns the resources configuration. |
| 637 | + """ |
| 638 | + |
| 639 | + # Call on_load_resources sending the current resources configuration. |
| 640 | + self.config['resources'] = self.on_load_resources(self.config['resources']) # noqa |
| 641 | + |
| 642 | + # Copy over for the template config also |
| 643 | + # TODO: Check relevancy of this line |
| 644 | + self.tpl_config['resources'] = deepcopy(self.config['resources']) |
| 645 | + |
| 646 | + # Keep track of UTC date of last time resources were loaded |
| 647 | + self.last_loaded_resources = datetime.now(timezone.utc) |
| 648 | + |
| 649 | + def reload_resources_if_necessary(self) -> None: |
| 650 | + """ |
| 651 | + Checks if the resources should be reloaded by calling overridable |
| 652 | + function 'on_load_resources_check' and then, when necessary, calls |
| 653 | + 'load_resources'. |
| 654 | + """ |
| 655 | + |
| 656 | + # If the resources should be reloaded |
| 657 | + if self.on_load_resources_check(self.last_loaded_resources): |
| 658 | + # Reload the resources |
| 659 | + self.load_resources() |
| 660 | + |
570 | 661 | def get_exception(self, status, headers, format_, code,
|
571 | 662 | description) -> Tuple[dict, int, str]:
|
572 | 663 | """
|
@@ -916,6 +1007,7 @@ def conformance(api, request: APIRequest) -> Tuple[dict, int, str]:
|
916 | 1007 |
|
917 | 1008 |
|
918 | 1009 | @jsonldify
|
| 1010 | +@pre_load_colls |
919 | 1011 | def describe_collections(api: API, request: APIRequest,
|
920 | 1012 | dataset=None) -> Tuple[dict, int, str]:
|
921 | 1013 | """
|
|
0 commit comments