Skip to content

Commit e0811a4

Browse files
author
Hanzhang Zeng (Roger)
committed
Added interface for extension registration
Fix nit Ensure extension can be loaded successfully Add unittests Modify documentations Change trace context attribute names Fix wrongly declared abstract method Accept Revert changes in Context Fix unittests
1 parent e8868a2 commit e0811a4

File tree

3 files changed

+424
-1
lines changed

3 files changed

+424
-1
lines changed

azure/functions/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from ._servicebus import ServiceBusMessage
1414
from ._durable_functions import OrchestrationContext, EntityContext
1515
from .meta import get_binding_registry
16+
from .extension import FuncExtension
1617

1718
# Import binding implementations to register them
1819
from . import blob # NoQA
@@ -54,7 +55,11 @@
5455
'TimerRequest',
5556

5657
# Middlewares
57-
'WsgiMiddleware'
58+
'WsgiMiddleware',
59+
60+
# Extensions
61+
'FuncExtension',
62+
'FuncExtensionInitError'
5863
)
5964

6065
__version__ = '1.6.0'

azure/functions/extension.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import abc
5+
import os
6+
from typing import Callable, List, Dict, NamedTuple
7+
from logging import Logger
8+
from ._abc import Context
9+
10+
11+
class FuncExtensionHookMeta(NamedTuple):
12+
ext_name: str
13+
impl: Callable
14+
15+
16+
# Defines kinds of hook that we support
17+
class FuncExtensionHooks(NamedTuple):
18+
before_invocation: List[FuncExtensionHookMeta] = []
19+
after_invocation: List[FuncExtensionHookMeta] = []
20+
21+
22+
class FuncExtension(abc.ABC):
23+
"""An abstract class defines the lifecycle hooks which to be implemented
24+
by customer's extension. Everytime when a new extension is initialized in
25+
customer's trigger, the _instances field will record it and will be
26+
executed by Python worker.
27+
"""
28+
_instances: Dict[str, FuncExtensionHooks] = {}
29+
30+
@abc.abstractmethod
31+
def __init__(self, trigger_name: str):
32+
"""Constructor for extension. This needs to be implemented and ensure
33+
super().__init__(trigger_name) is called.
34+
35+
The initializer serializes the extension to a tree. This speeds
36+
up the worker lookup and reduce the overhead on each invocation.
37+
_instances[<trigger_name>].<hook_name>.(ext_name, impl)
38+
39+
Parameters
40+
----------
41+
trigger_name: str
42+
The name of trigger the extension attaches to (e.g. HttpTrigger).
43+
"""
44+
ext_hooks = FuncExtension._instances.setdefault(
45+
trigger_name.lower(),
46+
FuncExtensionHooks()
47+
)
48+
49+
for hook_name in ext_hooks._fields:
50+
hook_impl = getattr(self, hook_name, None)
51+
if hook_impl is not None:
52+
getattr(ext_hooks, hook_name).append(FuncExtensionHookMeta(
53+
ext_name=self.__class__.__name__,
54+
impl=hook_impl
55+
))
56+
57+
# DO NOT decorate this with @abc.abstratmethod
58+
# since implementation is not mandatory
59+
def before_invocation(self, logger: Logger, context: Context,
60+
*args, **kwargs) -> None:
61+
"""A lifecycle hook to be implemented by the extension. This method
62+
will be called right before customer's function.
63+
64+
Parameters
65+
----------
66+
logger: logging.Logger
67+
A logger provided by Python worker. Extension developer should
68+
use this logger to emit telemetry to Azure Functions customers.
69+
context: azure.functions.Context
70+
This will include the function_name, function_directory and an
71+
invocation_id of this specific invocation.
72+
"""
73+
pass
74+
75+
# DO NOT decorate this with @abc.abstratmethod
76+
# since implementation is not mandatory
77+
def after_invocation(self, logger: Logger, context: Context,
78+
*args, **kwargs) -> None:
79+
"""A lifecycle hook to be implemented by the extension. This method
80+
will be called right after customer's function.
81+
82+
Parameters
83+
----------
84+
logger: logging.Logger
85+
A logger provided by Python worker. Extension developer should
86+
use this logger to emit telemetry to Azure Functions customers.
87+
context: azure.functions.Context
88+
This will include the function_name, function_directory and an
89+
invocation_id of this specific invocation.
90+
"""
91+
pass
92+
93+
@classmethod
94+
def get_hooks_of_trigger(cls, trigger_name: str) -> FuncExtensionHooks:
95+
"""Return all function extension hooks indexed by trigger name.
96+
97+
Parameters
98+
----------
99+
trigger_name: str
100+
The trigger name
101+
"""
102+
return cls._instances.get(trigger_name.lower(), FuncExtensionHooks())
103+
104+
@classmethod
105+
def register_to_trigger(cls, filename: str) -> 'FuncExtension':
106+
"""Register extension to a specific trigger. Derive trigger name from
107+
script filepath and AzureWebJobsScriptRoot environment variable.
108+
109+
Parameters
110+
----------
111+
filename: str
112+
The path to current trigger script. Usually, pass in __file__.
113+
114+
Returns
115+
-------
116+
FuncExtension
117+
The extension or its subclass
118+
"""
119+
script_root = os.getenv('AzureWebJobsScriptRoot')
120+
if script_root is None:
121+
raise ValueError(
122+
'AzureWebJobsScriptRoot environment variable is not defined. '
123+
'Please ensure the extension is running in Azure Functions.'
124+
)
125+
126+
try:
127+
trigger_name = os.path.split(
128+
os.path.relpath(
129+
os.path.abspath(filename),
130+
os.path.abspath(script_root)
131+
)
132+
)[0]
133+
except IndexError:
134+
raise ValueError(
135+
'Failed to parse trigger name from filename. Please ensure '
136+
'__file__ is passed into the filename argument'
137+
)
138+
139+
return cls(trigger_name)

0 commit comments

Comments
 (0)