Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion python/hello/Procfile

This file was deleted.

79 changes: 62 additions & 17 deletions python/hello/README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,73 @@
# Python 'hello' template
# Python HTTP Function

Welcome to your new Python function project! The boilerplate function
code can be found in [`func.py`](./func.py). This function will
simply print 'Hello, World!' if a request is received successfuly.
## Introduction

## Endpoints
A Python HTTP function built with ASGI protocol support. The implementation
provides a simple HTTP endpoint that responds with "Hello World!" to incoming
requests.

Running this function will expose three endpoints.
The function is structured as a class-based implementation with proper lifecycle
management, including configurable startup and shutdown hooks.

* `/` The endpoint for your function.
* `/health/readiness` The endpoint for a readiness health check
* `/health/liveness` The endpoint for a liveness health check
## Recommended Deployment

The health checks can be accessed in your browser at
[http://localhost:8080/health/readiness]() and
[http://localhost:8080/health/liveness]().
> [!NOTE]
> We recommend using the host builder.
> This feature is currently behind a flag because its not available for all the
> languages yet so you will need to enable it.

You can use `func invoke` to send an HTTP request to the function endpoint.
```bash
# Enable the host builder
export FUNC_ENABLE_HOST_BUILDER=1

# Deploy your code to cluster
# Make sure to set the builder to use it
func deploy --builder=host

# Local development and testing
func run --builder=host --container=false
```

## Customization

- This function uses the ASGI (Asynchronous Server Gateway Interface) 3.0
specification therefore its compatible with the signature `handle(scope, receive, send)`

### Lifecycle Management
The function provides lifecycle hooks:
- **start()**: Initialization hook called when function instances are created
- **stop()**: Cleanup hook, ensuring graceful termination and resource cleanup
- **alive()**: Health check exposed at `/health/liveness`
- **ready()**: Readiness check exposed at `/health/readiness`

## Testing

This function project includes a [unit test](./test_func.py). Update this
as you add business logic to your function in order to test its behavior.
The function includes unit tests that verify the HTTP handler behavior.
Tests are located in the `tests/` directory and use pytest with asyncio support.

To run the tests:

```console
python test_func.py
```bash
# Install dependencies (if not already installed)
pip install -e .

# Run tests
pytest

# Run tests with verbose output
pytest -v
```

### Test Structure

Tests are organized in the `tests/` directory with the current test file
`test_func.py` verifying that the ASGI handler returns a proper 200 OK response.
The testing framework uses pytest with asyncio support, configured in `pyproject.toml`.

### Writing New Tests

To add new tests, create files named `test_*.py` in the `tests/` directory.
For async functions, use the `@pytest.mark.asyncio` decorator. Mock ASGI
components by creating mock `scope`, `receive`, and `send` functions as shown
in the existing test. You can also test lifecycle methods like `start()`,
`stop()`, `alive()`, and `ready()` by calling them directly on a function instance.
3 changes: 0 additions & 3 deletions python/hello/app.sh

This file was deleted.

16 changes: 0 additions & 16 deletions python/hello/func.py

This file was deleted.

1 change: 1 addition & 0 deletions python/hello/function/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .func import new
72 changes: 72 additions & 0 deletions python/hello/function/func.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Function
import logging


def new():
""" New is the only method that must be implemented by a Function.
The instance returned can be of any name.
"""
return Function()


class Function:
def __init__(self):
""" The init method is an optional method where initialization can be
performed. See the start method for a startup hook which includes
configuration.
"""

async def handle(self, scope, receive, send):
""" Handle all HTTP requests to this Function other than readiness
and liveness probes."""

logging.info("OK: Request Received")

# echo the request to the calling client
await send({
'type': 'http.response.start',
'status': 200,
'headers': [
[b'content-type', b'text/plain'],
],
})
await send({
'type': 'http.response.body',
'body': 'Hello World!'.encode(),
})

def start(self, cfg):
""" start is an optional method which is called when a new Function
instance is started, such as when scaling up or during an update.
Provided is a dictionary containing all environmental configuration.
Args:
cfg (Dict[str, str]): A dictionary containing environmental config.
In most cases this will be a copy of os.environ, but it is
best practice to use this cfg dict instead of os.environ.
"""
logging.info("Function starting")

def stop(self):
""" stop is an optional method which is called when a function is
stopped, such as when scaled down, updated, or manually canceled. Stop
can block while performing function shutdown/cleanup operations. The
process will eventually be killed if this method blocks beyond the
platform's configured maximum studown timeout.
"""
logging.info("Function stopping")

def alive(self):
""" alive is an optional method for performing a deep check on your
Function's liveness. If removed, the system will assume the function
is ready if the process is running. This is exposed by default at the
path /health/liveness. The optional string return is a message.
"""
return True, "Alive"

def ready(self):
""" ready is an optional method for performing a deep check on your
Function's readiness. If removed, the system will assume the function
is ready if the process is running. This is exposed by default at the
path /health/rediness.
"""
return True, "Ready"
24 changes: 24 additions & 0 deletions python/hello/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[project]
name = "function"
description = ""
version = "0.1.0"
requires-python = ">=3.9"
readme = "README.md"
license = "MIT"
dependencies = [
"httpx",
"pytest",
"pytest-asyncio"
]
authors = [
{ name="Your Name", email="you@example.com"},
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.pytest.ini_options]
asyncio_mode = "strict"
asyncio_default_fixture_loop_scope = "function"

1 change: 0 additions & 1 deletion python/hello/requirements.txt

This file was deleted.

13 changes: 0 additions & 13 deletions python/hello/test_func.py

This file was deleted.

Binary file not shown.
38 changes: 38 additions & 0 deletions python/hello/tests/test_func.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
An example set of unit tests which confirm that the main handler (the
callable function) returns 200 OK for a simple HTTP GET.
"""
import pytest
from function import new


@pytest.mark.asyncio
async def test_function_handle():
f = new() # Instantiate Function to Test

sent_ok = False
sent_headers = False
sent_body = False

# Mock Send
async def send(message):
nonlocal sent_ok
nonlocal sent_headers
nonlocal sent_body

if message.get('status') == 200:
sent_ok = True

if message.get('type') == 'http.response.start':
sent_headers = True

if message.get('type') == 'http.response.body':
sent_body = True

# Invoke the Function
await f.handle({}, {}, send)

# Assert send was called
assert sent_ok, "Function did not send a 200 OK"
assert sent_headers, "Function did not send headers"
assert sent_body, "Function did not send a body"
Loading