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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Added
### Changed

- Simplified instanced functions by expecting a named 'handle' method.

### Deprecated
### Removed
### Fixed
Expand All @@ -19,13 +22,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [0.2.0] - 2024-11-06

### Added

- optional message returns from lifeycle methods "alive" and "ready"
- expanded development and release documentation


## [0.1.0] - 2024-10-28

### Added

- Initial Implementation of the Python HTTP Functions Middleware


9 changes: 6 additions & 3 deletions cmd/fhttp/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ async def handle(scope, receive, send):

# Example instanced handler
# This is the default expected by this test.
# The class can be named anything. See "new" below.
# The class can be named anything, but there must be a constructor named "new"
# which returns an object with an async method "handle" conforming to the ASGI
# callable's method signature.
class MyFunction:
async def __call__(self, scope, receive, send):
async def handle(self, scope, receive, send):
logging.info("OK")

await send({
Expand Down Expand Up @@ -68,7 +70,8 @@ def stop(self):

# Function instance constructor
# expected to be named exactly "new"
# Must return a callable which conforms to the ASGI spec.
# Must return a object which exposes a method "handle" which conforms to the
# ASGI callable spec.
def new():
""" new is the factory function (or constructor) which will create
a new function instance when invoked. This must be named "new", and the
Expand Down
13 changes: 7 additions & 6 deletions src/func_python/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

logging.basicConfig(level=DEFAULT_LOG_LEVEL)


def serve(f):
"""serve a function f by wrapping it in an ASGI web application
and starting. The function can be either a constructor for a functon
Expand All @@ -28,7 +29,7 @@ def serve(f):
raise
else:
raise ValueError("function must be either be a constructor 'new' or a "
"handler 'handle'.")
"handler function 'handle'.")


class DefaultFunction:
Expand All @@ -38,7 +39,7 @@ class DefaultFunction:
def __init__(self, handler):
self.handle = handler

async def __call__(self, scope, receive, send):
async def handle(self, scope, receive, send):
# delegate to the handler implementation provided during construction.
await self.handle(scope, receive, send)

Expand All @@ -47,6 +48,9 @@ class ASGIApplication():
def __init__(self, f):
self.f = f
self.stop_event = asyncio.Event()
if hasattr(self.f, "handle") is not True:
raise AttributeError( "Function must implement a 'handle' method.")

# Inform the user via logs that defaults will be used for health
# endpoints if no matchin methods were provided.
if hasattr(self.f, "alive") is not True:
Expand Down Expand Up @@ -125,10 +129,7 @@ async def __call__(self, scope, receive, send):
elif scope['path'] == '/health/readiness':
await self.handle_readiness(scope, receive, send)
else:
if callable(self.f):
await self.f(scope, receive, send)
else:
raise Exception("function does not implement handle")
await self.f.handle(scope, receive, send)
except Exception as e:
await send_exception(send, 500, f"Error: {e}")

Expand Down
6 changes: 4 additions & 2 deletions tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@

# Set a dynamic test URL using an environment variable
os.environ["LISTEN_ADDRESS"] = os.getenv("LISTEN_ADDRESS", "127.0.0.1:8081")

# Retrieve the LISTEN_ADDRESS for use in the tests
LISTEN_ADDRESS = os.getenv("LISTEN_ADDRESS")


def test_static():
"""
ensures that a user function developed using the default "static"
style (method signature) is served by the middleware.
"""

# Functoin
# Function
# An example minimal "static" user function which will be
# exposed on the network as an ASGI service by the middleware.
async def handle(scope, receive, send):
Expand Down Expand Up @@ -81,7 +83,7 @@ def test_instanced():
# An example standard "instanced" function (user's Function) which is
# exposed on the network as an ASGI service by the middleware.
class MyFunction:
async def __call__(self, scope, receive, send):
async def handle(self, scope, receive, send):
await send({
'type': 'http.response.start',
'status': 200,
Expand Down
Loading