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
1 change: 1 addition & 0 deletions .github/workflows/build-python-pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
cloud-service:
- "Azure Function App"
- "GCP Cloud Function"
- "AWS Lambda"
python-version:
- "3.12"
- "3.13"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Follow the prompts to configure your project.
<tr>
<td align="center"><img src="./.docs/imgs/python.svg" height="18" title="Python"></td>
<td align="center"><span title="Complete">✅</span></td>
<td align="center"><span title="Planned">📋</span></td>
<td align="center"><span title="Complete">✅</span></td>
<td align="center"><span title="Complete">✅</span></td>
</tr>
<tr>
Expand Down
3 changes: 2 additions & 1 deletion python/cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"author": "Code and Sorts (Colby Timm)",
"cloud_service": [
"Azure Function App",
"GCP Cloud Function"
"GCP Cloud Function",
"AWS Lambda"
],
"open_source_license": [
"MIT license",
Expand Down
17 changes: 15 additions & 2 deletions python/hooks/post_gen_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,28 @@

# Files to remove based on cloud service
if cloud_service == "Azure Function App":
# Remove GCP-specific files
# Remove GCP and AWS-specific files
files_to_remove = [
"main.py",
"lambda_app.py",
"template.yaml",
]
elif cloud_service == "GCP Cloud Function":
# Remove Azure-specific files
# Remove Azure and AWS-specific files
files_to_remove = [
"function_app.py",
"local.settings.json",
Comment thread
colbytimm marked this conversation as resolved.
"host.json",
"lambda_app.py",
"template.yaml",
]
elif cloud_service == "AWS Lambda":
# Remove Azure and GCP-specific files
files_to_remove = [
"function_app.py",
"local.settings.json",
"main.py",
"host.json",
]
else:
files_to_remove = []
Expand Down
5 changes: 5 additions & 0 deletions python/{{cookiecutter.project_class_name}}/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ run: ## Run function app.
@echo "Use 'poetry run functions-framework --target=<function_name> --source=main.py' to run specific functions"
@poetry run functions-framework --target=get_list --source=main.py --port=8080
{%- endif %}
{% if cookiecutter.cloud_service == 'AWS Lambda' -%}
@echo "🚀 Building and running Lambda API locally via SAM"
@sam build
@sam local start-api
{%- endif %}
Comment thread
colbytimm marked this conversation as resolved.

.PHONY: build
build: clean-build ## Build wheel file using poetry
Expand Down
80 changes: 80 additions & 0 deletions python/{{cookiecutter.project_class_name}}/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ This project is a Python-based REST API built using [Azure Function Apps](https:
{% if cookiecutter.cloud_service == 'GCP Cloud Function' -%}
This project is a Python-based REST API built using [Google Cloud Functions](https://cloud.google.com/functions/docs). The API leverages GCP's serverless architecture, allowing you to deploy and scale functions effortlessly in the cloud. The HTTP-triggered functions serve as the endpoints for the API, providing a seamless way to handle client requests.
{%- endif %}
{% if cookiecutter.cloud_service == 'AWS Lambda' -%}
This project is a Python-based REST API built using [AWS Lambda](https://docs.aws.amazon.com/lambda/) with API Gateway. The API leverages AWS's serverless architecture, allowing you to deploy and scale functions effortlessly in the cloud. The HTTP-triggered Lambda functions serve as the endpoints for the API, providing a seamless way to handle client requests.
{%- endif %}

The REST API has the following endpoints:
- GET (by ID)
Expand All @@ -24,6 +27,9 @@ Dependency management is handled using [Poetry](https://python-poetry.org/), ens
{% if cookiecutter.cloud_service == 'GCP Cloud Function' -%}
Dependency management can be handled using either [Poetry](https://python-poetry.org/) for development or requirements.txt for GCP deployment.
{%- endif %}
{% if cookiecutter.cloud_service == 'AWS Lambda' -%}
Dependency management is handled using [Poetry](https://python-poetry.org/), ensuring a streamlined and consistent environment for managing Python packages and their dependencies.
{%- endif %}

## Features

Expand All @@ -45,6 +51,15 @@ Dependency management can be handled using either [Poetry](https://python-poetry

- Firestore Database: This project uses Google Cloud Firestore as the NoSQL database.
{%- endif %}
{% if cookiecutter.cloud_service == 'AWS Lambda' -%}
- AWS Lambda: Utilizes AWS's serverless platform to create scalable and efficient endpoints with API Gateway HTTP triggers.

- Python-Based: Written entirely in Python, leveraging its rich ecosystem and libraries for rapid development.

- Poetry for Dependency Management: Manages all Python dependencies with Poetry, making the development environment consistent and easy to set up.

- DynamoDB: This project uses Amazon DynamoDB as the NoSQL database.
{%- endif %}

## Prerequisites

Expand Down Expand Up @@ -72,6 +87,17 @@ Dependency management can be handled using either [Poetry](https://python-poetry

- Firestore Database: Set up a Firestore database in your GCP project.
{%- endif %}
{% if cookiecutter.cloud_service == 'AWS Lambda' -%}
- [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html): To run the Lambda functions locally.

- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html): To deploy and manage AWS resources.

- [Poetry](https://python-poetry.org/): For dependency management and virtual environment setup.

- AWS Account: An active AWS account for deploying Lambda functions.

- DynamoDB Table: A DynamoDB table will be created automatically via the SAM template.
{%- endif %}

## Setup and Installation

Expand Down Expand Up @@ -158,6 +184,45 @@ Dependency management can be handled using either [Poetry](https://python-poetry
--set-env-vars GCP_PROJECT_ID=your-project-id,FIRESTORE_COLLECTION={{ cookiecutter.project_slug }}
```
{%- endif %}
{% if cookiecutter.cloud_service == 'AWS Lambda' -%}
1. Install AWS SAM CLI

Follow the [documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) to install the AWS SAM CLI based on your operating system.

2. Install Poetry

If you haven't already installed Poetry, you can do so by following the [official installation guide](https://python-poetry.org/docs/).

3. Install Dependencies

Install all dependencies and set up the virtual environment:

```console
make install
```

4. Run the API Locally

```console
make run
```

This command starts the local API Gateway using SAM CLI, where you can interact with your API endpoints.

Comment thread
colbytimm marked this conversation as resolved.
> **Note:** API endpoints that interact with DynamoDB require a running DynamoDB instance.
> For local development, you can use [DynamoDB Local](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html)
> or connect to a deployed DynamoDB table by configuring your AWS credentials and setting the
> `DYNAMODB_TABLE_NAME` environment variable in `template.yaml`.

5. Deploy to AWS

Build and deploy to AWS using SAM:

```console
sam build
sam deploy --guided
```
{%- endif %}

## Development Workflow

Expand Down Expand Up @@ -223,6 +288,21 @@ This is also run automatically in CI on every PR and push to main.
└── requirements.txt - Production dependencies for GCP deployment
```
{%- endif %}
{% if cookiecutter.cloud_service == 'AWS Lambda' -%}
```text
├── cookiecutter-template-python
│ ├── blueprints - Lambda handler methods
│ ├── controllers - Controllers
│ ├── errors - Custom errors
│ ├── models - Pydantic models
│ ├── repositories - DynamoDB repository
│ ├── services - Services
│ └── utils - Error detect & response generator utilities
├── lambda_app.py - Lambda entry point with API Gateway routing
└── template.yaml - AWS SAM template for deployment
```
{%- endif %}

## License

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
# GCP Cloud Functions don't use blueprints, see main.py for function exports
__all__ = []
{%- endif %}
{% if cookiecutter.cloud_service == 'AWS Lambda' -%}
# AWS Lambda doesn't use blueprints, see lambda_app.py for function routing
__all__ = []
{%- endif %}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import os
import logging
from controllers import {{ cookiecutter.project_class_name }}Controller
from services import {{ cookiecutter.project_class_name }}Service
from repositories import {{ cookiecutter.project_class_name }}Repository
from utils import detect_error, response_generator
{% if cookiecutter.cloud_service == 'Azure Function App' -%}
import azure.functions as func
from azure.cosmos import CosmosClient
Expand Down Expand Up @@ -31,10 +35,17 @@
collection = db.collection(collection_name)
repository = {{ cookiecutter.project_class_name }}Repository(collection)
{%- endif %}
from controllers import {{ cookiecutter.project_class_name }}Controller
from services import {{ cookiecutter.project_class_name }}Service
from repositories import {{ cookiecutter.project_class_name }}Repository
from utils import detect_error, response_generator
{% if cookiecutter.cloud_service == 'AWS Lambda' -%}
import boto3

# Initialize DynamoDB table resource with explicit region to avoid
# NoRegionError in local dev or misconfigured environments.
aws_region = os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION") or "us-east-1"
dynamodb = boto3.resource("dynamodb", region_name=aws_region)
table_name = os.getenv("DYNAMODB_TABLE_NAME", "{{ cookiecutter.project_slug }}")
table = dynamodb.Table(table_name)
repository = {{ cookiecutter.project_class_name }}Repository(table)
Comment thread
colbytimm marked this conversation as resolved.
{%- endif %}

service = {{ cookiecutter.project_class_name }}Service(repository)
controller = {{ cookiecutter.project_class_name }}Controller(service)
Expand All @@ -46,6 +57,10 @@ async def get_by_id(req: func.HttpRequest) -> func.HttpResponse:
{%- if cookiecutter.cloud_service == 'GCP Cloud Function' %}
def get_by_id(request: Request):
"""HTTP Cloud Function to get item by ID."""
{%- endif %}
{%- if cookiecutter.cloud_service == 'AWS Lambda' %}
def get_by_id(event):
"""Lambda handler to get item by ID."""
{%- endif %}
logging.info("Get {{ cookiecutter.project_endpoint }} by ID processed a request.")

Expand All @@ -55,6 +70,9 @@ def get_by_id(request: Request):
{%- endif %}
{%- if cookiecutter.cloud_service == 'GCP Cloud Function' %}
item = controller.get_by_id(request)
{%- endif %}
{%- if cookiecutter.cloud_service == 'AWS Lambda' %}
item = controller.get_by_id(event)
{%- endif %}
return response_generator(item)

Expand All @@ -69,6 +87,10 @@ async def get_list(req: func.HttpRequest) -> func.HttpResponse:
{%- if cookiecutter.cloud_service == 'GCP Cloud Function' %}
def get_list(request: Request):
"""HTTP Cloud Function to get all items."""
{%- endif %}
{%- if cookiecutter.cloud_service == 'AWS Lambda' %}
def get_list(event):
"""Lambda handler to get all items."""
{%- endif %}
logging.info("Get {{ cookiecutter.project_endpoint }} list processed a request.")

Expand All @@ -87,6 +109,10 @@ async def create(req: func.HttpRequest) -> func.HttpResponse:
{%- if cookiecutter.cloud_service == 'GCP Cloud Function' %}
def create(request: Request):
"""HTTP Cloud Function to create a new item."""
{%- endif %}
{%- if cookiecutter.cloud_service == 'AWS Lambda' %}
def create(event):
"""Lambda handler to create a new item."""
{%- endif %}
logging.info("Create item processed a request.")

Expand All @@ -96,6 +122,9 @@ def create(request: Request):
{%- endif %}
{%- if cookiecutter.cloud_service == 'GCP Cloud Function' %}
created_item = controller.create(request)
{%- endif %}
{%- if cookiecutter.cloud_service == 'AWS Lambda' %}
created_item = controller.create(event)
{%- endif %}
return response_generator(created_item, 201)

Expand All @@ -110,6 +139,10 @@ async def update(req: func.HttpRequest) -> func.HttpResponse:
{%- if cookiecutter.cloud_service == 'GCP Cloud Function' %}
def update(request: Request):
"""HTTP Cloud Function to update an existing item."""
{%- endif %}
{%- if cookiecutter.cloud_service == 'AWS Lambda' %}
def update(event):
"""Lambda handler to update an existing item."""
{%- endif %}
logging.info("Patch item processed a request.")

Expand All @@ -119,6 +152,9 @@ def update(request: Request):
{%- endif %}
{%- if cookiecutter.cloud_service == 'GCP Cloud Function' %}
updated_item = controller.update(request)
{%- endif %}
{%- if cookiecutter.cloud_service == 'AWS Lambda' %}
updated_item = controller.update(event)
{%- endif %}
return response_generator(updated_item, 201)

Expand All @@ -133,6 +169,10 @@ async def delete(req: func.HttpRequest) -> func.HttpResponse:
{%- if cookiecutter.cloud_service == 'GCP Cloud Function' %}
def delete(request: Request):
"""HTTP Cloud Function to soft delete an item."""
{%- endif %}
{%- if cookiecutter.cloud_service == 'AWS Lambda' %}
def delete(event):
"""Lambda handler to soft delete an item."""
{%- endif %}
logging.info("Delete item processed a request.")

Expand All @@ -148,6 +188,14 @@ def delete(request: Request):
controller.soft_delete(request)
return ("{{ cookiecutter.project_class_name }} deleted.", 200)
{%- endif %}
{%- if cookiecutter.cloud_service == 'AWS Lambda' %}
controller.soft_delete(event)
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": '{"message": "{{ cookiecutter.project_class_name }} deleted."}'
}
Comment thread
colbytimm marked this conversation as resolved.
{%- endif %}

except Exception as error:
return detect_error(error)
Loading