Skip to content

Commit

Permalink
WIP AWS deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
faisal-fawad committed Aug 18, 2024
1 parent 22d293c commit 0f0ab1c
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 4 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@
**/__pycache__
.pytest_cache
.ruff_cache
junit
junit
aws/dependencies
*.zip
84 changes: 84 additions & 0 deletions aws/build_lambda_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import inspect
import re
import os
import subprocess
import shutil
import zipfile
from imaginate_api.date.routes import images_by_date
from imaginate_api.utils import build_result, calculate_date
from imaginate_api.schemas.date_info import DateInfo


CWD = os.path.dirname(os.path.realpath(__file__))
LAMBDA_LIBRARIES = """import os
import json
from enum import Enum
from http import HTTPStatus
from base64 import b64encode
# External libraries from pymongo:
from pymongo import MongoClient
from gridfs import GridFS
from bson.objectid import ObjectId
"""
LAMBDA_SETUP = """db_name = 'imaginate_dev'
conn_uri = os.environ.get('MONGO_TOKEN')
client = MongoClient(conn_uri)
db = client[db_name]
fs = GridFS(db)
"""
LAMBDA_FUNC = """def handler(event, context):
if event and 'queryStringParameters' in event and event['queryStringParameters'] and 'day' in event['queryStringParameters']:
return images_by_date(event['queryStringParameters']['day'])
else:
return {'statusCode': HTTPStatus.BAD_REQUEST, 'body': json.dumps('Invalid date')}
"""
LAMBDA_SUBS = {
"abort": "return {'statusCode': HTTPStatus.BAD_REQUEST, 'body': json.dumps('Invalid date')}",
"@bp.route": "", # Remove this from function decorator
"return jsonify": "return {'statusCode': HTTPStatus.OK, 'body': json.dumps(out)}",
}


# Meta-program our source function to substitute Flask related libraries
def edit_source_function(source_function: str) -> str:
for sub in LAMBDA_SUBS:
source_function = re.sub(
r"^(\s*)" + sub + r".*$",
r"\g<1>" + LAMBDA_SUBS[sub],
source_function,
flags=re.MULTILINE,
)
return source_function.strip()


if __name__ == "__main__":
# The main function AWS Lambda will directly invoke
source_function = edit_source_function(inspect.getsource(images_by_date))

# Order all the required external code needed: helper functions, classes and source function
all_functions = [
inspect.getsource(DateInfo),
inspect.getsource(build_result),
inspect.getsource(calculate_date),
source_function,
]

# Save a .py file of our Lambda function code (mainly for verification purposes)
with open("aws/index.py", "w") as f:
f.write(LAMBDA_LIBRARIES + "\n") # Libaries defined as constants
f.write(LAMBDA_SETUP + "\n")
f.write("\n".join(all_functions) + "\n\n") # Functions retrieved from source code
f.write(LAMBDA_FUNC)

# Following documentation from here:
# https://www.mongodb.com/developer/products/atlas/awslambda-pymongo/
subprocess.run("mkdir dependencies", shell=True, cwd=CWD)
subprocess.run(
"pip install --upgrade --target ./dependencies pymongo", shell=True, cwd=CWD
)
shutil.make_archive("aws", "zip", "aws/dependencies")
zf = zipfile.ZipFile("aws.zip", "a")
zf.write("aws/index.py", "index.py")
zf.close()
print("aws.zip successfully saved at root directory!")
121 changes: 121 additions & 0 deletions aws/cloudformation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Sourced from: https://gist.github.com/magnetikonline/c314952045eee8e8375b82bc7ec68e88
AWSTemplateFormatVersion: "2010-09-09"
Description: Imaginate API Gateway and Lambda function

Parameters:
apiGatewayName:
Type: String
Default: ImaginateApi
apiGatewayStageName:
Type: String
AllowedPattern: '[a-z0-9]+'
Default: call
apiGatewayHTTPMethod:
Type: String
Default: GET
lambdaFunctionName:
Type: String
AllowedPattern: '[a-zA-Z0-9]+[a-zA-Z0-9-]+[a-zA-Z0-9]+'
Default: get-images
mongoToken:
Type: String
NoEcho: true

Resources:
apiGateway:
Type: AWS::ApiGateway::RestApi
Properties:
Description: Example API Gateway
EndpointConfiguration:
Types:
- REGIONAL
Name: !Ref apiGatewayName

apiGatewayRootMethod:
Type: AWS::ApiGateway::Method
Properties:
AuthorizationType: NONE
RequestParameters:
method.request.querystring.day: true
HttpMethod: !Ref apiGatewayHTTPMethod
Integration:
IntegrationHttpMethod: POST
Type: AWS_PROXY
RequestParameters:
integration.request.querystring.day: method.request.querystring.day
Uri: !Sub
- arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations
- lambdaArn: !GetAtt lambdaFunction.Arn
ResourceId: !GetAtt apiGateway.RootResourceId
RestApiId: !Ref apiGateway

apiGatewayDeployment:
Type: AWS::ApiGateway::Deployment
DependsOn:
- apiGatewayRootMethod
Properties:
RestApiId: !Ref apiGateway
StageName: !Ref apiGatewayStageName

lambdaFunction:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
def handler(event, context):
pass
Description: Get images by date
FunctionName: !Ref lambdaFunctionName
Handler: index.handler
Role: !GetAtt lambdaIAMRole.Arn
Runtime: python3.12
Environment:
Variables:
MONGO_TOKEN: !Ref mongoToken

lambdaApiGatewayInvoke:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt lambdaFunction.Arn
Principal: apigateway.amazonaws.com
# NOTE: If the route is NOT at API Gateway root, `SourceArn` would take the form of:
# arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${apiGateway}/${apiGatewayStageName}/${apiGatewayHTTPMethod}/PATH_PART
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${apiGateway}/${apiGatewayStageName}/${apiGatewayHTTPMethod}/

lambdaIAMRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Policies:
- PolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Effect: Allow
Resource:
- !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${lambdaFunctionName}:*
PolicyName: lambda

lambdaLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${lambdaFunctionName}
RetentionInDays: 90

Outputs:
apiGatewayInvokeURL:
Value: !Sub https://${apiGateway}.execute-api.${AWS::Region}.amazonaws.com/${apiGatewayStageName}
lambdaArn:
Value: !GetAtt lambdaFunction.Arn
80 changes: 80 additions & 0 deletions aws/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import os
import json
from enum import Enum
from http import HTTPStatus
from base64 import b64encode

# External libraries from pymongo:
from pymongo import MongoClient
from gridfs import GridFS
from bson.objectid import ObjectId

db_name = "imaginate_dev"
conn_uri = os.environ.get("MONGO_TOKEN")
client = MongoClient(conn_uri)
db = client[db_name]
fs = GridFS(db)


class DateInfo(Enum):
START_DATE = 1722484800 # Timestamp for August 1st, 2024
SECONDS_PER_DAY = 86400


def build_result(
_id: ObjectId, real: bool, date: int, theme: str, status: str, filename: str
):
return {
"filename": filename,
"url": "image/read/" + str(_id),
"real": real,
"date": date,
"theme": theme,
"status": status,
}


def calculate_date(day: str | int | None):
if day is not None:
if isinstance(day, str):
day = int(day)
if day >= DateInfo.START_DATE.value:
return day
return DateInfo.START_DATE.value + day * DateInfo.SECONDS_PER_DAY.value
return None


def images_by_date(day):
try:
date = calculate_date(day)
if not date:
return {"statusCode": HTTPStatus.BAD_REQUEST, "body": json.dumps("Invalid date")}
except ValueError:
return {"statusCode": HTTPStatus.BAD_REQUEST, "body": json.dumps("Invalid date")}

res = fs.find({"date": date})
out = []
for document in res:
current_res = build_result(
document._id,
document.real,
document.date,
document.theme,
document.status,
document.filename,
)
encoded_data = b64encode(document.read())
current_res["data"] = encoded_data.decode("utf-8")
out.append(current_res)
return {"statusCode": HTTPStatus.OK, "body": json.dumps(out)}


def handler(event, context):
if (
event
and "queryStringParameters" in event
and "day" in event["queryStringParameters"]
):
return images_by_date(event["queryStringParameters"]["day"])
else:
return {"statusCode": HTTPStatus.BAD_REQUEST, "body": json.dumps("Invalid date")}
4 changes: 2 additions & 2 deletions imaginate_api/date/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from imaginate_api.extensions import fs
from imaginate_api.utils import build_result, calculate_date
from http import HTTPStatus
import base64
from base64 import b64encode

bp = Blueprint("date", __name__)

Expand All @@ -28,7 +28,7 @@ def images_by_date(day):
document.status,
document.filename,
)
encoded_data = base64.b64encode(document.read())
encoded_data = b64encode(document.read())
current_res["data"] = encoded_data.decode("utf-8")
out.append(current_res)
return jsonify(out)
Expand Down
3 changes: 2 additions & 1 deletion imaginate_api/schemas/date_info.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from enum import Enum


class DateInfo(Enum):
START_DATE = 1722484800 # timestamp for august 1st, 2024
START_DATE = 1722484800 # Timestamp for August 1st, 2024
SECONDS_PER_DAY = 86400

0 comments on commit 0f0ab1c

Please sign in to comment.