/py-functions

🔥🤫🤐

Primary LanguagePythonMIT LicenseMIT

Requirements

  • Python 3.6 or higher
  • Pip (usually installed alongside Python)

Setup

  • Install virtualenv if you already haven't. Makes it a lot easier to keep Python dependencies and modules isolated from the rest of your environment.

    pip install --user virtualenv
    
    # After this you might have to add the virtualenv binary
    # to your $PATH.
    
  • Create and activate a new virtual environment.

virtualenv -p python3 py3
source py3/bin/activate
  • Install dependencies.
pip install -r requirements.txt

Running the example

  • firebase_functions contains the experimental Functions SDK for Python.

  • sample.py contains some sample user code written using the Functions SDK.

  • Run the codegen tool to generate an entrypoint. Save the output to a new app.py file.

python ./firebase_functions/codegen.py ./sample.py > app.py
  • Start the Flask server to serve the generated entrypoint.
gunicorn -b :8080 app:app
  • Start a separate server to serve the backend.yaml.

    gunicorn -b :8081 app:backend_yaml
    
  • To trigger the HTTP function:

curl localhost:8080/http_function
  • To trigger the PubSub function:
curl -X POST -d '{"uid": "alice"}' localhost:8080/pubsub_function
  • To get the discovery yaml (currently served on the same port):
curl localhost:8081/backend.yaml

Implementation notes

Decorators

Python supports higher-order functions. That is functions can accept and return other functions. In the following example, say_hello() accepts another function

def say_hello(func):
  print(f"Hello, {func()}")

def alice():
  return "Alice"

say_hello(alice) # Prints "Hello, Alice"

Decorators provide syntactic sugar around this type of higher-order function manipulation. Here's the same example implemented as a decorator.

def say_hello(func):
  def wrapper():
    print(f"Hello, {func()}")
  return wrapper

@say_hello
def alice():
  return "Alice"

alice() # Prints "Hello, Alice"

Here, say_hello() is a decorator function. When applied on another function it augments the behavior of that function. It essentially replaces alice() in-place with say_hello(alice). Note that the result of say_hello(alice) in this case is a new wrapper() function.

Replacing functions in-place like above has some ugly consequences. For instance, inspecting the replaced function with print(alice) yields the output <function say_hello.<locals>.wrapper at 0x107153430> which exposes the internals of the decorator. A more idiomatic way to do this is to implement the decorator using the utils provided in the built-in functools API:

import functools

def say_hello(func):
  @functools.wraps(func)
  def wrapper():
    print(f"Hello, {func()}")
  return wrapper

@say_hello
def alice():
  return "Alice"

This retains the original context information of the alice() function, and as a result print(alice) will produce <function alice at 0x103cb7430>.

In this SDK we use decorators to add extra metadata to the functions implemented by developers. Specifically, each decorator adds a new firebase_metadata property to developer's functions. Later, our codegen tool will extract this metadata to generate the necessary manifest files for deployment.

Decorators with arguments

Consider the following decorator.

def https(_func=None, *, min_instances=None, max_instances=None, memory_mb=None):

Here aguments after * are keyword-only arguments. That is they must be specified with keywords (names) -- e.g. min_instances=1. Just passing the value 1 without the keyword would result in an error.

In the above signature all the arguments are optional. Therefore a developer may apply it without any arguments, or with some arguments. When applied without arguments, Python will pass the target decorated function as _func. But when applied with one or more arguments _func actually gets set to None. Therefore the decorator implementation must explicitly handle both cases:

if _func is None:
  return https_with_options

return https_with_options(_func)

Now consider the following decorator.

def pubsub(*, topic, min_instances=None):

This also defines a decorator that requires 2 keyword arguments. But one of them (topic) is defined without a default value making it a required argument. As a result, when applied, the target decorated function never gets passed into this decorator. So we essentially have only one case to handle:

return pubsub_with_topic