Skip to content

Commit 797c6e1

Browse files
authored
Example api code updates (#3)
This adds some more complete code examples and usage of various pants goals
1 parent 6182279 commit 797c6e1

19 files changed

+490
-169
lines changed

3rdparty/python/default.lock

+203-161
Large diffs are not rendered by default.

BUILD

+9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1+
python_requirement(
2+
name="jmespath",
3+
requirements=["jmespath"],
4+
)
5+
16
python_requirements(
27
name="root",
38
source="pyproject.toml",
9+
overrides={
10+
"aws-lambda-powertools": {"dependencies": [":jmespath"]},
11+
},
412
)
13+

README.md

+26
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,31 @@ Pants commands are called _goals_. You can get a list of goals with
2222
pants help goals
2323
```
2424

25+
## Example Goals
26+
The following goals are configured in this repo.
27+
28+
Run Formatter and linters:
29+
```
30+
pants lint fmt ::
31+
pants --changed-since=HEAD lint fmt :: # Only run on changed files
32+
```
33+
34+
Run formatter and linters with fixes:
35+
```
36+
pants fix ::
37+
pants --changed-since=HEAD fix :: # Only run on changed files
38+
```
39+
40+
Run Tests
41+
```
42+
pants test ::
43+
pants --changed-since=HEAD test :: # Only run on changed files
44+
```
45+
46+
Package AWS Lambda Zip files
47+
```
48+
pants package ::
49+
```
50+
2551
Most goals take arguments to run on. To run on a single directory, use the directory name with `:`
2652
at the end. To recursively run on a directory and all its subdirectories, add `::` to the end.

aws/BUILD

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
python_test_utils(
2+
name="test_utils",
3+
)

aws/conftest.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from dataclasses import dataclass
2+
3+
import pytest
4+
5+
6+
@pytest.fixture
7+
def lambda_context():
8+
@dataclass
9+
class LambdaContext:
10+
function_name: str = 'test'
11+
memory_limit_in_mb: int = 128
12+
invoked_function_arn: str = 'arn:aws:lambda:eu-west-1:809313241:function:test'
13+
aws_request_id: str = '52fdfc07-2182-154f-163f-5f0f9a621d72'
14+
15+
return LambdaContext()

aws/example_rest_api/README.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Example REST API on AWS
2+
3+
This is a simplified example of a REST API that can be run on AWS using:
4+
5+
- [API Gateway](https://aws.amazon.com/api-gateway/)
6+
- [AWS Lambda](https://aws.amazon.com/lambda/)
7+
8+
This example includes the following lambda functions:
9+
10+
- A Get Item Lambda Function:
11+
Used solely for retrieving items
12+
13+
- A Manage Items Lambda Function:
14+
This lambda handles the rest of the CRUD operations, creating, updating, and deleting items.
15+
16+
Pants allows a lot of flexibility with Lambda Functions. You can use single purpose lambda functions, such as the
17+
get_item function, or a lambda that serves several purposes.
18+
19+
Pants will package both internal and external dependencies across your repo. Keep in mind AWS Lambda [Quotas](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html) for package sizes.
20+
21+
22+
23+
> **_Note:_** This very simplified example uses hard coded examples. We are just storing items in memory. This would likely be a database call in a production app.

aws/example_rest_api/api/BUILD

+13
Original file line numberDiff line numberDiff line change
@@ -1 +1,14 @@
11
python_sources()
2+
3+
python_aws_lambda_function(
4+
name="get_item_lambda",
5+
runtime="python3.11",
6+
handler="get_item.py:lambda_handler",
7+
)
8+
9+
10+
python_aws_lambda_function(
11+
name="manage_items_lambda",
12+
runtime="python3.11",
13+
handler="manage_items.py:lambda_handler",
14+
)

aws/example_rest_api/api/create_user.py

-2
This file was deleted.

aws/example_rest_api/api/get_item.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from aws_lambda_powertools import Logger
2+
from aws_lambda_powertools.event_handler import ApiGatewayResolver, Response
3+
4+
from aws.example_rest_api.models.item import Item
5+
6+
app = ApiGatewayResolver()
7+
logger = Logger()
8+
9+
items: dict[int, Item] = {
10+
1: Item(item_id=1, name='test_item', description='test item description'),
11+
}
12+
13+
14+
@app.get('/items/<item_id>')
15+
def get_item(item_id: str):
16+
item = items.get(int(item_id))
17+
18+
if item:
19+
return Response(status_code=200, body=item.model_dump())
20+
21+
return Response(status_code=404)
22+
23+
24+
@logger.inject_lambda_context
25+
def lambda_handler(event, context):
26+
return app.resolve(event, context)

aws/example_rest_api/api/get_user.py

-2
This file was deleted.
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from aws_lambda_powertools import Logger
2+
from aws_lambda_powertools.event_handler import ApiGatewayResolver, Response
3+
4+
from aws.example_rest_api.models.item import Item
5+
6+
app = ApiGatewayResolver(enable_validation=True)
7+
logger = Logger()
8+
9+
10+
items: dict[int, Item] = {
11+
1: Item(item_id=1, name='test_item', description='test item description'),
12+
}
13+
14+
15+
@app.delete('/items/<item_id>')
16+
def delete_item(item_id: str):
17+
item = items.get(int(item_id))
18+
if item:
19+
logger.info(f'Removing Item: {item.name}')
20+
del items[item.item_id]
21+
22+
return Response(status_code=204)
23+
24+
logger.info('Unable to process request, Item not found')
25+
return Response(status_code=400)
26+
27+
28+
@app.post('/items')
29+
def create_item(request_body: Item):
30+
if request_body.item_id in items:
31+
logger.info('Item already exists, unable to create')
32+
return Response(status_code=400)
33+
34+
items[request_body.item_id] = Item(**request_body.model_dump())
35+
36+
return Response(
37+
status_code=201,
38+
headers={'Location': f'https://example.com/items/{request_body.item_id}'},
39+
)
40+
41+
42+
@app.patch('/items/<item_id>')
43+
def update_item_details(item_id: str, item_update: Item):
44+
item = items.get(int(item_id))
45+
if item:
46+
logger.info(f'Updating {item.name}', update_request=item_update.model_dump())
47+
items[item_update.item_id] = item_update
48+
return Response(status_code=200)
49+
50+
logger.error('unable to find existing item')
51+
return Response(status_code=404)
52+
53+
54+
def lambda_handler(event, context):
55+
return app.resolve(event, context)

aws/example_rest_api/api/update_user.py

-2
This file was deleted.

aws/example_rest_api/models/item.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from pydantic import BaseModel
2+
3+
4+
class Item(BaseModel):
5+
item_id: int
6+
name: str
7+
description: str

aws/example_rest_api/models/user.py

Whitespace-only changes.

aws/tests/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python_tests()

aws/tests/test_get_item.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from aws.example_rest_api.api.get_item import lambda_handler
2+
3+
4+
def test_retrieve_item_from_lambda_handler(lambda_context):
5+
payload = {
6+
'version': '1.0',
7+
'resource': '/items/{item_id}',
8+
'path': '/items/1',
9+
'httpMethod': 'GET',
10+
'pathParameters': {'item_id': '1'},
11+
}
12+
13+
response = lambda_handler(payload, lambda_context)
14+
15+
assert response['body'] == {
16+
'item_id': 1,
17+
'name': 'test_item',
18+
'description': 'test item description',
19+
}
20+
assert response['statusCode'] == 200

aws/tests/test_manage_items.py

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from unittest.mock import patch
2+
3+
from aws.example_rest_api.api.manage_items import lambda_handler
4+
from aws.example_rest_api.models.item import Item
5+
6+
7+
@patch(
8+
'aws.example_rest_api.api.manage_items.items',
9+
{2: Item(item_id=2, name='delete_me', description='delete_me')},
10+
)
11+
def test_delete_item(lambda_context):
12+
payload = {
13+
'version': '1.0',
14+
'resource': '/items/{item_id}',
15+
'path': '/items/2',
16+
'httpMethod': 'DELETE',
17+
'pathParameters': {'item_id': '2'},
18+
}
19+
response = lambda_handler(payload, lambda_context)
20+
21+
assert response['statusCode'] == 204
22+
23+
24+
def test_create_item(lambda_context):
25+
payload_body = Item(item_id=3, name='new_item', description='new item').model_dump_json()
26+
payload = {
27+
'version': '1.0',
28+
'path': '/items',
29+
'httpMethod': 'POST',
30+
'body': payload_body,
31+
'resource': '/items',
32+
}
33+
34+
response = lambda_handler(payload, lambda_context)
35+
36+
assert response['statusCode'] == 201
37+
38+
39+
@patch(
40+
'aws.example_rest_api.api.manage_items.items',
41+
{9: Item(item_id=9, name='change_me', description='change_me')},
42+
)
43+
def test_update_item(lambda_context):
44+
payload_body = Item(
45+
item_id=9, name='changed_item', description='changed item'
46+
).model_dump_json()
47+
payload = {
48+
'version': '1.0',
49+
'path': '/items/9',
50+
'httpMethod': 'PATCH',
51+
'body': payload_body,
52+
'resource': '/items/9',
53+
}
54+
55+
response = lambda_handler(payload, lambda_context)
56+
57+
assert response

pants.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ pants_version = "2.20.0"
33

44
backend_packages = [
55
"pants.backend.python",
6-
"pants.backend.python.lint.black",
76
"pants.backend.awslambda.python",
7+
"pants.backend.experimental.python.lint.ruff.check",
8+
"pants.backend.experimental.python.lint.ruff.format",
89
]
910

1011
[python]
1112
enable_resolves=true
1213
interpreter_constraints = ["CPython==3.11.*"]
14+

pyproject.toml

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,32 @@
11
[project]
2+
name = "pants_example_serverless"
23
dependencies = [
3-
"aws-lambda-powertools[all]",
4+
"aws-lambda-powertools",
5+
"pydantic",
46
]
7+
8+
[project.optional-dependencies]
9+
dev = [
10+
"ruff",
11+
]
12+
test = [
13+
"pytest",
14+
]
15+
16+
[tool.ruff]
17+
line-length = 100
18+
19+
[tool.ruff.lint]
20+
select = [
21+
# pycodestyle
22+
"E",
23+
# Pyflakes
24+
"F",
25+
# isort
26+
"I",
27+
]
28+
29+
[tool.ruff.format]
30+
quote-style = "single"
31+
indent-style = "tab"
32+
docstring-code-format = true

0 commit comments

Comments
 (0)