|
| 1 | +# Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | +# Licensed under the MIT License. |
| 3 | + |
| 4 | +from typing import Optional, Union, Dict, List |
| 5 | +import abc |
| 6 | +import json |
| 7 | +from .app_extension_hooks import AppExtensionHooks |
| 8 | +from .func_extension_hooks import FuncExtensionHooks |
| 9 | +from .extension_hook_meta import ExtensionHookMeta |
| 10 | +from .extension_scope import ExtensionScope |
| 11 | +from .function_extension_exception import FunctionExtensionException |
| 12 | + |
| 13 | + |
| 14 | +class ExtensionMeta(abc.ABCMeta): |
| 15 | + """The metaclass handles extension registration. |
| 16 | +
|
| 17 | + AppExtension is regsistered in __init__, it is applied to all triggers. |
| 18 | + FuncExtension is registered in __call__, as users need to instantiate it |
| 19 | + inside hook script. |
| 20 | +
|
| 21 | + After registration, the extension class will be flatten into the following |
| 22 | + structure to speed up worker lookup: |
| 23 | + _func_exts[<trigger_name>].<hook_name>.(ext_name, ext_impl) |
| 24 | + (e.g. _func_exts['HttpTrigger'].pre_invocation.ext_impl) |
| 25 | +
|
| 26 | + _app_exts.<hook_name>.(ext_name, ext_impl) |
| 27 | + (e.g. _app_exts.pre_invocation_app_level.ext_impl) |
| 28 | +
|
| 29 | + The extension tree information is stored in _info for diagnostic |
| 30 | + purpose. The dictionary is serializible to json: |
| 31 | + _info['FuncExtension']['<Trigger>'] = list(<Extension>) |
| 32 | + _info['AppExtension'] = list(<Extension>) |
| 33 | + """ |
| 34 | + _func_exts: Dict[str, FuncExtensionHooks] = {} |
| 35 | + _app_exts: Optional[AppExtensionHooks] = None |
| 36 | + _info: Dict[str, Union[Dict[str, List[str]], List[str]]] = {} |
| 37 | + |
| 38 | + def __init__(cls, *args, **kwargs): |
| 39 | + """Executes on 'import extension', once the AppExtension class is |
| 40 | + loaded, call the setup() method and add the life-cycle hooks into |
| 41 | + _app_exts. |
| 42 | + """ |
| 43 | + super(ExtensionMeta, cls).__init__(*args, **kwargs) |
| 44 | + scope = ExtensionMeta._get_extension_scope(cls) |
| 45 | + |
| 46 | + # Only register application extension here |
| 47 | + if scope is ExtensionScope.APPLICATION: |
| 48 | + ExtensionMeta._register_application_extension(cls) |
| 49 | + |
| 50 | + def __call__(cls, *args, **kwargs): |
| 51 | + """Executes on 'inst = extension(__file__)', once the FuncExtension |
| 52 | + class is instantiate, overwrite the __init__() method and add the |
| 53 | + instance into life-cycle hooks. |
| 54 | + """ |
| 55 | + scope = ExtensionMeta._get_extension_scope(cls) |
| 56 | + |
| 57 | + # Only register function extension here |
| 58 | + if scope is ExtensionScope.FUNCTION: |
| 59 | + instance = super(ExtensionMeta, cls).__call__(*args, **kwargs) |
| 60 | + ExtensionMeta._register_function_extension(instance) |
| 61 | + return instance |
| 62 | + elif scope is ExtensionScope.APPLICATION: |
| 63 | + raise FunctionExtensionException( |
| 64 | + f'Python worker extension with scope:{scope} should not be' |
| 65 | + 'instantiable. Please access via class method directly.' |
| 66 | + ) |
| 67 | + else: |
| 68 | + raise FunctionExtensionException( |
| 69 | + f'Python worker extension:{cls.__name__} is not properly ' |
| 70 | + 'implemented from AppExtensionBase or FuncExtensionBase.' |
| 71 | + ) |
| 72 | + |
| 73 | + @classmethod |
| 74 | + def get_function_hooks(cls, name: str) -> Optional[FuncExtensionHooks]: |
| 75 | + """Return all function extension hooks indexed by trigger name. |
| 76 | +
|
| 77 | + Returns |
| 78 | + ------- |
| 79 | + Optional[FuncExtensionHooks]: |
| 80 | + Example to look up a certain life-cycle name: |
| 81 | + get_function_hooks('HttpTrigger').pre_invocation.ext_name |
| 82 | + """ |
| 83 | + return cls._func_exts.get(name.lower()) |
| 84 | + |
| 85 | + @classmethod |
| 86 | + def get_application_hooks(cls) -> Optional[AppExtensionHooks]: |
| 87 | + """Return all application hooks |
| 88 | +
|
| 89 | + Returns |
| 90 | + ------- |
| 91 | + Optional[AppExtensionHooks]: |
| 92 | + Example to look up a certain life-cycle name: |
| 93 | + get_application_hooks().pre_invocation_app_level.ext_name |
| 94 | + """ |
| 95 | + return cls._app_exts |
| 96 | + |
| 97 | + @classmethod |
| 98 | + def get_registered_extensions_json(cls) -> str: |
| 99 | + """Return a json string of the registered |
| 100 | +
|
| 101 | + Returns |
| 102 | + ------- |
| 103 | + str: |
| 104 | + The json string will be constructed in a structure of |
| 105 | + { |
| 106 | + "FuncExtension": { |
| 107 | + "<TriggerA>": [ |
| 108 | + "ExtensionName" |
| 109 | + ] |
| 110 | + }, |
| 111 | + "AppExtension": [ |
| 112 | + "ExtensionName" |
| 113 | + ] |
| 114 | + } |
| 115 | + """ |
| 116 | + return json.dumps(cls._info) |
| 117 | + |
| 118 | + @classmethod |
| 119 | + def _get_extension_scope(cls, extension) -> ExtensionScope: |
| 120 | + """Return the scope of an extension""" |
| 121 | + return getattr(extension, '_scope', # type: ignore |
| 122 | + ExtensionScope.UNKNOWN) |
| 123 | + |
| 124 | + @classmethod |
| 125 | + def _set_hooks_for_function(cls, trigger_name: str, ext): |
| 126 | + ext_hooks = cls._func_exts.setdefault( |
| 127 | + trigger_name.lower(), |
| 128 | + cls._create_default_function_hook() |
| 129 | + ) |
| 130 | + |
| 131 | + # Flatten extension class to cls._func_exts |
| 132 | + for hook_name in ext_hooks._fields: |
| 133 | + hook_impl = getattr(ext, hook_name, None) |
| 134 | + if hook_impl is not None: |
| 135 | + hook_meta = ExtensionHookMeta( |
| 136 | + ext_name=ext.__class__.__name__, |
| 137 | + ext_impl=hook_impl, |
| 138 | + ) |
| 139 | + getattr(ext_hooks, hook_name).append(hook_meta) |
| 140 | + |
| 141 | + @classmethod |
| 142 | + def _set_hooks_for_application(cls, ext): |
| 143 | + if cls._app_exts is None: |
| 144 | + cls._app_exts = cls._create_default_app_hook() |
| 145 | + |
| 146 | + # Check for definition in AppExtensionHooks NamedTuple |
| 147 | + for hook_name in cls._app_exts._fields: |
| 148 | + hook_impl = getattr(ext, hook_name, None) |
| 149 | + if hook_impl is not None: |
| 150 | + getattr(cls._app_exts, hook_name).append(ExtensionHookMeta( |
| 151 | + ext_name=ext.__name__, |
| 152 | + ext_impl=hook_impl |
| 153 | + )) |
| 154 | + |
| 155 | + @classmethod |
| 156 | + def _register_function_extension(cls, extension): |
| 157 | + """Flatten the function extension structure into function hooks""" |
| 158 | + # Should skip registering FuncExtensionBase, cannot use isinstance(), |
| 159 | + # referring to func_extension_hooks introduces a dependency cycle |
| 160 | + if extension.__class__.__name__ == 'FuncExtensionBase': |
| 161 | + return |
| 162 | + |
| 163 | + trigger_name = extension._trigger_name |
| 164 | + cls._set_hooks_for_function(trigger_name, extension) |
| 165 | + |
| 166 | + # Record function extension information |
| 167 | + hooks_info = cls._info.setdefault( # type: ignore |
| 168 | + 'FuncExtension', {}).setdefault(trigger_name, []) |
| 169 | + hooks_info.append(extension.__class__.__name__) |
| 170 | + |
| 171 | + @classmethod |
| 172 | + def _register_application_extension(cls, extension): |
| 173 | + """Flatten the application extension structure into function hooks""" |
| 174 | + # Should skip registering AppExtensionBase, cannot use isinstance(), |
| 175 | + # referring to app_extension_hooks introduces a dependency cycle |
| 176 | + if extension.__name__ == 'AppExtensionBase': |
| 177 | + return |
| 178 | + |
| 179 | + if getattr(extension, 'init', None): |
| 180 | + extension.init() |
| 181 | + |
| 182 | + cls._set_hooks_for_application(extension) |
| 183 | + |
| 184 | + # Record application extension information |
| 185 | + hooks_info = cls._info.setdefault('AppExtension', []) |
| 186 | + hooks_info.append(extension.__name__) # type: ignore |
| 187 | + |
| 188 | + @classmethod |
| 189 | + def _create_default_function_hook(cls) -> FuncExtensionHooks: |
| 190 | + return FuncExtensionHooks( |
| 191 | + post_function_load=[], |
| 192 | + pre_invocation=[], |
| 193 | + post_invocation=[] |
| 194 | + ) |
| 195 | + |
| 196 | + @classmethod |
| 197 | + def _create_default_app_hook(cls) -> AppExtensionHooks: |
| 198 | + return AppExtensionHooks( |
| 199 | + post_function_load_app_level=[], |
| 200 | + pre_invocation_app_level=[], |
| 201 | + post_invocation_app_level=[] |
| 202 | + ) |
0 commit comments