Skip to content

Commit

Permalink
Remove unneeded requirements.txt file
Browse files Browse the repository at this point in the history
Move all azure-related files to ./azure/ directory
Add an azure publish script that will automatically build the required directory structure and generate the requirements.txt file when needed, then execute the FUNC_CLI publish operation.
Adds a README.md in the /azure/ directory with more details.
  • Loading branch information
ashleysommer committed Aug 27, 2024
1 parent 86a9c36 commit 16e1c7a
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 11 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ venv/
.vscode/
.idea/
.git/
build/
test_*.py
.github/
Dockerfile
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ __pycache__/
.pytest_cache/
.env*
dist/
build/
!.env-template
rdf/
http/
Expand Down
7 changes: 7 additions & 0 deletions azure/.funcignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.vscode/
.venv/
.idea/
__pycache__/
__pycache__
local.settings.json

39 changes: 39 additions & 0 deletions azure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Prez Azure Function-App deployment files

This directory contains the files required to deploy Prez as an Azure Function-App, as well as a Dockerfile that
can be used to build a container image for deploying the app as an Azure Container App.

## Publishing
There is a publish.sh script that can be used to publish the app to Azure.
To call it, make sure you are not in the "azure" directory, instead run the script from the root of the project.

```bash
./azure/publish.sh --extra-options <function-app-name>
```
The FunctionAppName is the name of the Azure Function-App that you want to publish to.
Note, the FunctionAppName must be the last argument to the script, after any optional arguments.

This script will perform the following steps:
1. Create a ./build directory
2. Copy the required azure function files from the ./azure directory into the ./build directory
* ./azure/function_app.py
* ./azure/patched_asgi_function_wrapper.py
* ./azure/host.json
* ./azure/.funcignore
3. Copy the local prez module source code into the ./build directory
4. Copy the .env file into the ./build directory if it exists
5. Copy the pyproject.toml and poetry.lock files into the ./build directory
6. Generate the requirements.txt file using poetry
7. Publish the app to the Azure Function-App (using remote build)

**extra-options** can be used to pass additional arguments to the azure publish command. (Eg, the `--subscription` argument)

_Note:_ the script automatically adds the `--build remote` argument to the publish command, you don't need to specify it.

## Building the Docker container image

To build the Docker container image, run the following command from the root of the project:

```bash
docker build -t <image-name> -f azure/azure_functions.Dockerfile .
```
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ RUN pip3 install poetry==${POETRY_VERSION}
RUN mkdir -p /build
WORKDIR /build

COPY . .
COPY .. .
RUN poetry build

RUN mkdir -p /home/site/wwwroot
Expand Down Expand Up @@ -52,7 +52,7 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get -qq update && \
bash

WORKDIR /home/site/wwwroot
COPY requirements.txt pyproject.toml host.json function_app.py ./
COPY pyproject.toml poetry.lock azure/host.json azure/function_app.py azure/patched_asgi_function_wrapper.py ./

ENTRYPOINT []
CMD ["/opt/startup/start_nonappservice.sh"]
29 changes: 22 additions & 7 deletions function_app.py → azure/function_app.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import os
import azure.functions as func
import sys
import pathlib
import logging

cwd = pathlib.Path(__file__).parent
if cwd.name == "azure":
# We are running from the repo source directory
# assume running locally, we need to add the parent
# directory to the Python Path
sys.path.append(str(cwd.parent))

import azure.functions as func

try:
from prez.app import assemble_app
except ImportError:
except ImportError as e:
logging.exception("Cannot import prez")
assemble_app = None


if assemble_app is None:
raise RuntimeError(
"Cannot import prez in the Azure function app. Check requirements.py and pyproject.toml."
"Cannot import prez in the Azure function app. Check requirements.txt and pyproject.toml."
)

from patched_asgi_function_wrapper import AsgiFunctionApp

# This is the base URL path that Prez routes will stem from
# must _start_ in a slash, but _not end_ in slash, eg: /prez
Expand All @@ -32,7 +43,7 @@

prez_app = assemble_app(root_path=ROOT_PATH)

app = func.AsgiFunctionApp(app=prez_app, http_auth_level=auth_level)
app = AsgiFunctionApp(app=prez_app, http_auth_level=auth_level)

if __name__ == "__main__":
from azure.functions import HttpRequest, Context
Expand All @@ -41,7 +52,11 @@
req = HttpRequest("GET", "/catalogs", headers={}, body=b"")
context = dict()
loop = asyncio.get_event_loop()

task = app.middleware.handle_async(req, context)
fns = app.get_functions()
assert len(fns) == 1
fn_def = fns[0]
fn = fn_def.get_user_function()
task = fn(req, context)
resp = loop.run_until_complete(task)
print(resp)

File renamed without changes.
File renamed without changes.
63 changes: 63 additions & 0 deletions azure/patched_asgi_function_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from typing import Union, TYPE_CHECKING
from copy import copy
import azure.functions as func
from azure.functions.decorators.http import HttpMethod
from azure.functions._http_asgi import AsgiMiddleware, AsgiRequest, AsgiResponse
from azure.functions._http_wsgi import WsgiMiddleware
from azure.functions._abc import Context
from azure.functions import HttpRequest

# -------------------
# Create a patched AsgiFunctionApp to fix the ASGI scope state issue
# -------------------
# See https://github.com/Azure/azure-functions-python-worker/issues/1566
class MyAsgiMiddleware(AsgiMiddleware):
async def _handle_async(self, req, context):
asgi_request = AsgiRequest(req, context)
scope = asgi_request.to_asgi_http_scope()
# shallow copy the state as-per the ASGI spec
scope["state"] = copy(self.state) # <-- this is the patch, add the state to the scope
asgi_response = await AsgiResponse.from_app(self._app,
scope,
req.get_body())
return asgi_response.to_func_response()

# -------------------
# Create a patched AsgiFunctionApp to fix the double-slash route issue
# -------------------
# See https://github.com/Azure/azure-functions-python-worker/issues/1310
class AsgiFunctionApp(func.AsgiFunctionApp):
def __init__(self, app, http_auth_level):
super(AsgiFunctionApp, self).__init__(None, http_auth_level=http_auth_level)
self._function_builders.clear()
self.middleware = MyAsgiMiddleware(app)
self._add_http_app(self.middleware)
self.startup_task_done = False

def _add_http_app(
self, http_middleware: Union[AsgiMiddleware, WsgiMiddleware]
) -> None:
"""Add an Asgi app integrated http function.
:param http_middleware: :class:`WsgiMiddleware`
or class:`AsgiMiddleware` instance.
:return: None
"""

asgi_middleware: AsgiMiddleware = http_middleware

@self.http_type(http_type="asgi")
@self.route(
methods=(method for method in HttpMethod),
auth_level=self.auth_level,
route="{*route}", # <-- this is the patch, removed the leading slash from the route
)
async def http_app_func(req: HttpRequest, context: Context):
if not self.startup_task_done:
success = await asgi_middleware.notify_startup()
if not success:
raise RuntimeError("ASGI middleware startup failed.")
self.startup_task_done = True

return await asgi_middleware.handle_async(req, context)
51 changes: 51 additions & 0 deletions azure/publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/bin/bash
DEFAULT_FUNC=$(which func)
DEFAULT_POETRY=$(which poetry)
FUNC_CLI=${FUNC_CLI:-"$DEFAULT_FUNC"}
POETRY=${POETRY:-"$DEFAULT_POETRY"}

ARG_COUNT="$#"
if [[ "$ARG_COUNT" -lt 1 ]] ; then
# Publishes the function app code to the remote FunctionAppName on Azure
echo "Usage: $0 [optional publish arguments] <FunctionAppName>"
exit 1
fi

FUNC_APP_NAME="${@: -1}"
# Set args to all args minus the function app name
set -- "${@:1:$(($#-1))}"
CWD="$(pwd)"
BASE_CWD="${CWD##*/}"
if [[ "$BASE_CWD" = "azure" ]] ; then
echo "Do not run this script from within the azure directory"
echo "Run from the root of the repo"
echo "eg: ./azure/build.sh"
exit 1
fi

if [[ -z "$FUNC_CLI" ]] ; then
echo "func cli not found, specify the location using env FUNC_CLI"
exit 1
fi

if [[ -z "$POETRY" ]] ; then
echo "poetry not found. Local poetry>=1.8.2 is required to generate the requirements.txt file"
echo "specify the location using env POETRY"
exit 1
fi

mkdir -p build
rm -rf build/*
cp ./azure/function_app.py ./azure/patched_asgi_function_wrapper.py ./azure/.funcignore ./azure/host.json ./azure/local.settings.json build/
cp ./pyproject.toml ./poetry.lock ./build
cp -r ./prez ./build
if [[ -f "./.env" ]] ; then
cp ./.env ./build
fi
cd ./build
"$POETRY" export --without-hashes --format=requirements.txt > requirements.txt
echo "generated requirements.txt"
cat ./requirements.txt
"$FUNC_CLI" azure functionapp publish "$FUNC_APP_NAME" --build remote "$@"
cd ..
echo "You can now delete the build directory if you wish."
2 changes: 0 additions & 2 deletions requirements.txt

This file was deleted.

0 comments on commit 16e1c7a

Please sign in to comment.