Skip to content

feat: add dev command #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 11, 2023
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@ def hello_world(event, context):
The configuration is done by passing arguments to the decorator.
To view which arguments are supported, head over to this [documentation](https://serverless-api-framework-python.readthedocs.io/) page.

### Local testing

Before deploying, you can run your functions locally with the dev command:

```console
scw-serverless dev app.py
```

This runs a local Flask server with your Serverless handlers. If no `relative_url` is defined for a function, it will be served on `/name` with `name` being the name of your Python function.

By default, this runs Flask in debug mode which includes hot-reloading. You can disable this behavior by passing the `--no-debug` flag.

### Deploying

When you are ready, you can deploy your function with the `scw-serverless` CLI tool:

```console
Expand Down
25 changes: 24 additions & 1 deletion docs/source/deploying.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
Deploying
=========

After writing your functions, the included CLI tool `scw-serverless` helps deploy your application on Scaleway.
After writing your functions, the included CLI tool `scw-serverless` helps test and deploy your application on Scaleway.

Running locally
---------------

You can test your functions locally before deploying with the `dev` command:

.. code-block:: console

scw-serverless dev app.py

This will start a local Flask server with your functions that will behave similarly to Scaleway Functions.

By default, functions are served by `/name` on port 8080 with the name being the name of the Python function.

You can then use your favorite tools to query the functions:

.. code-block:: console

# For a function named def handle()...
curl http://localhost:8080/handle

This command allows you to test your code, but as this test environment is not quite the same as Scaleway Functions,
there might be slight differences when deploying.

Deploy
------
Expand Down
2 changes: 1 addition & 1 deletion examples/hello_world/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ def handle(_event: dict[str, Any], _content: dict[str, Any]) -> dict[str, Any]:
context (dict): function call metadata
"""

return {"message": "Hello from Scaleway functions using Serverless API Framework"}
return {"body": "Hello from Scaleway functions using Serverless API Framework"}
116 changes: 99 additions & 17 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ include = ["CHANGELOG.md"]
scw-serverless = "scw_serverless.cli:cli"

[tool.poetry.dependencies]
python = "^3.9"
python = ">=3.9,<3.12"
click = "^8.1.3"
scaleway = ">=0.7,<0.13"
scaleway-functions-python = "^0.2.0"
requests = "^2.28.2"
typing-extensions = { version = "^4.4.0", python = "<3.11" }

Expand Down
30 changes: 29 additions & 1 deletion scw_serverless/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import click
from scaleway import ScalewayException

from scw_serverless import deployment, loader, logger
import scw_serverless
from scw_serverless import app, deployment, loader, local_app, logger
from scw_serverless.dependencies_manager import DependenciesManager
from scw_serverless.gateway.gateway_manager import GatewayManager

Expand Down Expand Up @@ -145,3 +146,30 @@ def deploy(
sdk_client=client,
)
manager.update_routes()


@cli.command()
@CLICK_ARG_FILE
@click.option(
"--port",
"-p",
"port",
default=8080,
show_default=True,
help="Set port to listen on.",
)
@click.option(
"--debug/--no-debug",
"debug",
default=True,
show_default=True,
help="Run Flask in debug mode.",
)
def dev(file: Path, port: int, debug: bool) -> None:
"""Run functions locally with Serverless Local Testing."""
app.Serverless = local_app.ServerlessLocal
scw_serverless.Serverless = local_app.ServerlessLocal
Comment on lines +170 to +171
Copy link
Contributor Author

@cyclimse cyclimse May 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is of course very hacky but it's hard to find a better way as:

  • the instantiation of the Serverless class is done in the client's code
  • the binding to the handlers is done via decorators, and thus already done when we import a global instance (via loader.load_app_instance) so we need to "act" before that

My first approach was to add a handler parameter to the functions which would store the python handler as an object. This doesn't work well with multiprocessing (can't pickle a Python function) and I don't like it (may introduce side effects when deploying). Ideally, local testing should not modify the deploy command.

The second approach could have been a global Python variable LOCAL_TESTING=False that changes the behavior of the Serverless class. But this results in some conditionals imports as we do not want to import the local testing framework in app.py to avoid packaging issues when deploying.

So finally we override the class directly with a derived class. This is not ideal but it's quite clean compared to those alternatives.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yes, a bit of black magic but it works for now 🧙

app_instance = cast(
local_app.ServerlessLocal, loader.load_app_instance(file.resolve())
)
app_instance.local_server.serve(port=port, debug=debug)
4 changes: 2 additions & 2 deletions scw_serverless/config/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class Function:
"""Representation of a Scaleway function."""

name: str
handler: str # Path to the handler
handler_path: str
environment_variables: Optional[dict[str, str]] = None
min_scale: Optional[int] = None
max_scale: Optional[int] = None
Expand Down Expand Up @@ -85,7 +85,7 @@ def from_handler(
gateway_route = GatewayRoute(url, http_methods=args.get("http_methods"))
return Function(
name=to_valid_function_name(handler.__name__),
handler=module_to_path(handler.__module__) + "." + handler.__name__,
handler_path=module_to_path(handler.__module__) + "." + handler.__name__,
environment_variables=args.get("env"),
min_scale=args.get("min_scale"),
max_scale=args.get("max_scale"),
Expand Down
4 changes: 2 additions & 2 deletions scw_serverless/deployment/api_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def create_function(
max_scale=function.max_scale,
memory_limit=function.memory_limit,
timeout=function.timeout,
handler=function.handler,
handler=function.handler_path,
description=function.description,
secret_environment_variables=self._get_secrets_from_dict(
function.secret_environment_variables
Expand All @@ -110,7 +110,7 @@ def update_function(
max_scale=function.max_scale,
memory_limit=function.memory_limit,
timeout=function.timeout,
handler=function.handler,
handler=function.handler_path,
description=function.description,
secret_environment_variables=self._get_secrets_from_dict(
function.secret_environment_variables
Expand Down
Loading