Develop Python worker extensions for Azure Functions

Azure Functions lets you integrate custom behaviors as part of Python function execution. This feature enables you to create business logic that customers can easily use in their own function apps. To learn more, see the Python developer reference. Worker extensions are supported in both the v1 and v2 Python programming models.

In this tutorial, you'll learn how to:

  • Create an application-level Python worker extension for Azure Functions.
  • Consume your extension in an app the way your customers do.
  • Package and publish an extension for consumption.

Prerequisites

Before you start, you must meet these requirements:

Create the Python Worker extension

The extension you create reports the elapsed time of an HTTP trigger invocation in the console logs and in the HTTP response body.

Folder structure

The folder for your extension project should be like the following structure:

<python_worker_extension_root>/
 | - .venv/
 | - python_worker_extension_timer/
 | | - __init__.py
 | - setup.py
 | - readme.md
Folder/file Description
.venv/ (Optional) Contains a Python virtual environment used for local development.
python_worker_extension/ Contains the source code of the Python worker extension. This folder contains the main Python module to be published into PyPI.
setup.py Contains the metadata of the Python worker extension package.
readme.md Contains the instruction and usage of your extension. This content is displayed as the description in the home page in your PyPI project.

Configure project metadata

First you create setup.py, which provides essential information about your package. To make sure that your extension is distributed and integrated into your customer's function apps properly, confirm that 'azure-functions >= 1.7.0, < 2.0.0' is in the install_requires section.

In the following template, you should change author, author_email, install_requires, license, packages, and url fields as needed.

from setuptools import find_packages, setup
setup(
    name='python-worker-extension-timer',
    version='1.0.0',
    author='Your Name Here',
    author_email='your@email.here',
    classifiers=[
        'Intended Audience :: End Users/Desktop',
        'Development Status :: 5 - Production/Stable',
        'Intended Audience :: End Users/Desktop',
        'License :: OSI Approved :: Apache Software License',
        'Programming Language :: Python',
        'Programming Language :: Python :: 3.7',
        'Programming Language :: Python :: 3.8',
        'Programming Language :: Python :: 3.9',
        'Programming Language :: Python :: 3.10',
    ],
    description='Python Worker Extension Demo',
    include_package_data=True,
    long_description=open('readme.md').read(),
    install_requires=[
        'azure-functions >= 1.7.0, < 2.0.0',
        # Any additional packages that will be used in your extension
    ],
    extras_require={},
    license='MIT',
    packages=find_packages(where='.'),
    url='https://your-github-or-pypi-link',
    zip_safe=False,
)

Next, you'll implement your extension code in the application-level scope.

Implement the timer extension

Add the following code in python_worker_extension_timer/__init__.py to implement the application-level extension:

import typing
from logging import Logger
from time import time
from azure.functions import AppExtensionBase, Context, HttpResponse
class TimerExtension(AppExtensionBase):
    """A Python worker extension to record elapsed time in a function invocation
    """

    @classmethod
    def init(cls):
        # This records the starttime of each function
        cls.start_timestamps: typing.Dict[str, float] = {}

    @classmethod
    def configure(cls, *args, append_to_http_response:bool=False, **kwargs):
        # Customer can use TimerExtension.configure(append_to_http_response=)
        # to decide whether the elapsed time should be shown in HTTP response
        cls.append_to_http_response = append_to_http_response

    @classmethod
    def pre_invocation_app_level(
        cls, logger: Logger, context: Context,
        func_args: typing.Dict[str, object],
        *args, **kwargs
    ) -> None:
        logger.info(f'Recording start time of {context.function_name}')
        cls.start_timestamps[context.invocation_id] = time()

    @classmethod
    def post_invocation_app_level(
        cls, logger: Logger, context: Context,
        func_args: typing.Dict[str, object],
        func_ret: typing.Optional[object],
        *args, **kwargs
    ) -> None:
        if context.invocation_id in cls.start_timestamps:
            # Get the start_time of the invocation
            start_time: float = cls.start_timestamps.pop(context.invocation_id)
            end_time: float = time()
            # Calculate the elapsed time
            elapsed_time = end_time - start_time
            logger.info(f'Time taken to execute {context.function_name} is {elapsed_time} sec')
            # Append the elapsed time to the end of HTTP response
            # if the append_to_http_response is set to True
            if cls.append_to_http_response and isinstance(func_ret, HttpResponse):
                func_ret._HttpResponse__body += f' (TimeElapsed: {elapsed_time} sec)'.encode()

This code inherits from AppExtensionBase so that the extension applies to every function in the app. You could have also implemented the extension on a function-level scope by inheriting from FuncExtensionBase.

The init method is a class method that's called by the worker when the extension class is imported. You can do initialization actions here for the extension. In this case, a hash map is initialized for recording the invocation start time for each function.

The configure method is customer-facing. In your readme file, you can tell your customers when they need to call Extension.configure(). The readme should also document the extension capabilities, possible configuration, and usage of your extension. In this example, customers can choose whether the elapsed time is reported in the HttpResponse.

The pre_invocation_app_level method is called by the Python worker before the function runs. It provides the information from the function, such as function context and arguments. In this example, the extension logs a message and records the start time of an invocation based on its invocation_id.

Similarly, the post_invocation_app_level is called after function execution. This example calculates the elapsed time based on the start time and current time. It also overwrites the return value of the HTTP response.

Create a readme.md

Create a readme.md file in the root of your extension project. This file contains the instructions and usage of your extension. The readme.md content is displayed as the description in the home page in your PyPI project.

# Python Worker Extension Timer

In this file, tell your customers when they need to call `Extension.configure()`.

The readme should also document the extension capabilities, possible configuration,
and usage of your extension.

Consume your extension locally

Now that you've created an extension, you can use it in an app project to verify it works as intended.

Create an HTTP trigger function

  1. Create a new folder for your app project and navigate to it.

  2. From the appropriate shell, such as Bash, run the following command to initialize the project:

    func init --python
    
  3. Use the following command to create a new HTTP trigger function that allows anonymous access:

    func new -t HttpTrigger -n HttpTrigger -a anonymous
    

Activate a virtual environment

  1. Create a Python virtual environment, based on OS as follows:

    python3 -m venv .venv
    
  2. Activate the Python virtual environment, based on OS as follows:

    source .venv/bin/activate
    

Configure the extension

  1. Install remote packages for your function app project using the following command:

    pip install -r requirements.txt
    
  2. Install the extension from your local file path, in editable mode as follows:

    pip install -e <PYTHON_WORKER_EXTENSION_ROOT>
    

    In this example, replace <PYTHON_WORKER_EXTENSION_ROOT> with the root file location of your extension project.

    When a customer uses your extension, they'll instead add your extension package location to the requirements.txt file, as in the following examples:

    # requirements.txt
    python_worker_extension_timer==1.0.0
    
  3. Open the local.settings.json project file and add the following field to Values:

    "PYTHON_ENABLE_WORKER_EXTENSIONS": "1" 
    

    When running in Azure, you instead add PYTHON_ENABLE_WORKER_EXTENSIONS=1 to the app settings in the function app.

  4. Add following two lines before the main function in __init.py__ file for the v1 programming model, or in the function_app.py file for the v2 programming model:

    from python_worker_extension_timer import TimerExtension
    TimerExtension.configure(append_to_http_response=True)
    

    This code imports the TimerExtension module and sets the append_to_http_response configuration value.

Verify the extension

  1. From your app project root folder, start the function host using func host start --verbose. You should see the local endpoint of your function in the output as https://localhost:7071/api/HttpTrigger.

  2. In the browser, send a GET request to https://localhost:7071/api/HttpTrigger. You should see a response like the following, with the TimeElapsed data for the request appended.

    This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response. (TimeElapsed: 0.0009996891021728516 sec)
    

Publish your extension

After you've created and verified your extension, you still need to complete these remaining publishing tasks:

  • Choose a license.
  • Create a readme.md and other documentation.
  • Publish the extension library to a Python package registry or a version control system (VCS).

To publish your extension to PyPI:

  1. Run the following command to install twine and wheel in your default Python environment or a virtual environment:

    pip install twine wheel
    
  2. Remove the old dist/ folder from your extension repository.

  3. Run the following command to generate a new package inside dist/:

    python setup.py sdist bdist_wheel
    
  4. Run the following command to upload the package to PyPI:

    twine upload dist/*
    

    You may need to provide your PyPI account credentials during upload. You can also test your package upload with twine upload -r testpypi dist/*. For more information, see the Twine documentation.

After these steps, customers can use your extension by including your package name in their requirements.txt.

For more information, see the official Python packaging tutorial.

Examples

  • You can view completed sample extension project from this article in the python_worker_extension_timer sample repository.

  • OpenCensus integration is an open-source project that uses the extension interface to integrate telemetry tracing in Azure Functions Python apps. See the opencensus-python-extensions-azure repository to review the implementation of this Python worker extension.

Next steps

For more information about Azure Functions Python development, see the following resources: