Skip to content

Commit 8fac62b

Browse files
committed
multiple final fixes + better readme
1 parent 9b33f5f commit 8fac62b

File tree

10 files changed

+153
-29
lines changed

10 files changed

+153
-29
lines changed

.dockerignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
test
1+
example
2+
.git
3+
.idea

Dockerfile

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
FROM python:3.6 as builder
22

3-
43
COPY requirements.txt requirements.txt
54
RUN pip install -r requirements.txt
65

7-
COPY . /srv
8-
96

107
FROM lambci/lambda:python3.6
11-
128
WORKDIR /srv
139

1410
USER root
1511

1612
RUN yum install -y git
1713

1814
COPY --from=builder /usr/local/lib/python3.6/site-packages /var/lang/lib/python3.6/site-packages
19-
COPY --from=builder /srv /srv
20-
15+
COPY . /srv
2116

17+
# let's clear previous entrypoint
2218
ENTRYPOINT []
2319
CMD ["python", "-u", "app.py"]

README.md

+87-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,88 @@
11
# python-lambda-local-server
2-
A thin wrapper around python-lambda-local package to run as a server instead cli. This package is designed to be run by Docker.
2+
![build](https://img.shields.io/docker/build/valian/python-lambda-local-server.svg)
3+
A Docker image to help with development of a local AWS lambda Python functions.
4+
Based on the [lambci/lambda](https://hub.docker.com/r/lambci/lambda/) image.
5+
Created, because I haven't found an easy way to run lambda function locally.
6+
7+
Features:
8+
* Automatically installs packages from requirements.txt during startup
9+
* Mirrored AWS Lambda environment thanks to lambci images
10+
* Code is reloaded for each submitted event
11+
* Available simple Web UI for better workflow
12+
13+
# Usage
14+
15+
Go to directory with your lambda Python code. Next, run this command:
16+
17+
```bash
18+
docker run -it \
19+
-p 8080:8080 \
20+
-v lambda-packages-cache:/packages/ \
21+
-v $PWD:/var/task/ \
22+
valian/python-lambda-local-server
23+
```
24+
25+
It should automatically install packages from your requirements.txt and start web sever written in aiohttp.
26+
To see web UI, head to http://localhost:8080. You should see a simple web UI for testing your lambda function.
27+
28+
![Event UI](https://raw.githubusercontent.com/valian/python-lambda-local-server/master/pictures/event_ui.png)
29+
30+
Assuming that you have your lambda entrypoint in `lambda.py` and handler is named `handler`, you should
31+
type it into desired inputs. Next, add JSON body that should be passed to the function as an event, and press 'Submit event'.
32+
Result and logs should be visible on the right panel after a moment.
33+
34+
35+
# Example
36+
37+
First, clone this repository:
38+
39+
```bash
40+
git clone https://github.com/Valian/python-lambda-local-server
41+
cd python-lambda-local-server
42+
```
43+
44+
Next, start server with a proper volume mounted:
45+
```bash
46+
docker run -it \
47+
-p 8080:8080 \
48+
-v $PWD/example:/var/task/ \
49+
valian/python-lambda-local-server
50+
```
51+
52+
## Web UI usage
53+
54+
Go to http://localhost:8080. You should see something like this:
55+
56+
![Event UI](https://raw.githubusercontent.com/valian/python-lambda-local-server/master/pictures/event_ui.png)
57+
58+
Specify file, handler and event and click `Submit event`.
59+
It invokes handler and prints execution logs and return value.
60+
61+
62+
Api gateway tab automatically formats event to an AWS API Gateway format:
63+
![API UI](https://raw.githubusercontent.com/valian/python-lambda-local-server/master/pictures/api_ui.png)
64+
65+
## Console example
66+
67+
You can of course use `curl` to submit events. Start sever and type:
68+
69+
```bash
70+
curl -XPOST localhost:8080 -d '{"event": {"url": "https://example.com"}, "file": "handler.handler"}'
71+
72+
{
73+
"stdout": {
74+
"statusCode": 200,
75+
"url": "https://example.com/"
76+
},
77+
"stderr": "START RequestId: b1891caf-c22b-4ce6-8639-e74862adae30 Version: $LATEST\nINFO:root:Starting request\nINFO:root:Request done, status code: 200\nEND RequestId: b1891caf-c22b-4ce6-8639-e74862adae30\nREPORT RequestId: b1891caf-c22b-4ce6-8639-e74862adae30 Duration: 803 ms Billed Duration: 900 ms Memory Size: 1536 MB Max Memory Used: 23 MB\n"
78+
}
79+
```
80+
81+
# TODO
82+
* [ ] Add support for `Serverless.yml` file
83+
* [ ] Cleanup code, better error codes
84+
* [ ] Support for chaining lambda calls either through AWS SDK or API endpoint
85+
86+
# License
87+
88+
MIT
File renamed without changes.

example/handler.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import json
2+
import logging
3+
import requests
4+
5+
6+
def init_logging():
7+
# lambci adds root logging handler, we have to remove it first
8+
root_logger = logging.getLogger()
9+
for h in root_logger.handlers:
10+
root_logger.removeHandler(h)
11+
logging.basicConfig(level=logging.INFO)
12+
13+
14+
def make_request(url: str) -> dict:
15+
logging.info("Starting request")
16+
response = requests.get(url=url)
17+
logging.info(f"Request done, status code: {response.status_code}")
18+
return {
19+
"statusCode": response.status_code,
20+
"url": response.url
21+
}
22+
23+
24+
def handler(event, context):
25+
init_logging()
26+
logging.info(f"Event: {event}")
27+
return make_request(
28+
url=event.get('url', 'http://example.com')
29+
)
30+
31+
32+
def api_handler(event, context):
33+
init_logging()
34+
logging.info(f"Event: {event}")
35+
body = event.get('body', '')
36+
event = json.loads(body)
37+
url = event.get('url', 'http://example.com')
38+
response_data = make_request(url=url)
39+
return {
40+
'statusCode': response_data['statusCode'],
41+
'isBase64Encoded': False,
42+
'headers': {'Content-Type': 'application/json'},
43+
'body': json.dumps(response_data)
44+
}
File renamed without changes.

pictures/api_ui.png

63.7 KB
Loading

pictures/event_ui.png

55.3 KB
Loading

requirements.py

+17-12
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,21 @@ def get_requirements_directory(self, requirements_path):
3939
return None
4040

4141
def ensure_installed(self, force_reinstall=False):
42-
req_dir = self.directory
43-
if not req_dir or not path.exists(req_dir) or force_reinstall:
44-
logger.info(f"Updating requirements for tag '{self.tag}'...")
45-
# If previous requirements exists, let's remove them
46-
shutil.rmtree(req_dir, ignore_errors=True)
47-
new_requirements_dir = self.get_requirements_directory(self.requirements_path)
48-
subprocess.run([
49-
"pip", "install",
50-
"-t", new_requirements_dir,
51-
"-r", self.requirements_path])
52-
shutil.copy(self.requirements_path, self.cached_requirements_path)
42+
if not path.exists(self.requirements_path):
43+
logger.info(f"No requirements.txt found, skipping package installation")
5344
else:
54-
logger.info(f"Requirements not changed, skipping update for tag '{self.tag}'...")
45+
if self.directory and path.exists(self.directory) and not force_reinstall:
46+
logger.info(f"Requirements not changed, skipping update for tag '{self.tag}'...")
47+
else:
48+
self._install_packages()
49+
50+
def _install_packages(self):
51+
logger.info(f"Updating requirements for tag '{self.tag}'...")
52+
# If previous requirements exists, let's remove them
53+
shutil.rmtree(self.directory, ignore_errors=True)
54+
new_requirements_dir = self.get_requirements_directory(self.requirements_path)
55+
subprocess.run([
56+
"python", "-m", "pip", "install",
57+
"-t", new_requirements_dir,
58+
"-r", self.requirements_path])
59+
shutil.copy(self.requirements_path, self.cached_requirements_path)

test/handler.py

-9
This file was deleted.

0 commit comments

Comments
 (0)