Skip to content

Commit

Permalink
Adding CreateClientFunction
Browse files Browse the repository at this point in the history
  • Loading branch information
enrique committed May 16, 2024
1 parent f4017cc commit dc94c59
Show file tree
Hide file tree
Showing 23 changed files with 1,063 additions and 0 deletions.
61 changes: 61 additions & 0 deletions .github/workflows/clients_pipeline.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: clients_pipeline

on:
push:
paths:
- 'src/clients/**'
branches:
- 'main'
- 'feature**'
workflow_dispatch:

jobs:
run_unit_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
make install
working-directory: ./tests
- name: Run tests
run: |
make test
working-directory: ./tests

development:
if: startsWith(github.event.ref, 'refs/heads/main')
uses: ./.github/workflows/pipeline_template.yaml
needs: [run_unit_tests]
with:
sam_deploy_overrides: "StageName=development"
stack_name: ${{ vars.CLIENTS_STACK_NAME }}-development
sam_template: src/clients/template.yaml
aws_region: us-east-1
pipeline_execution_role: ${{ vars.PIPELINE_EXECUTION_ROLE_DEV }}
cloudformation_execution_role: ${{ vars.CLOUDFORMATION_EXECUTION_ROLE_DEV }}
artifacts_bucket: ${{ vars.ARTIFACTS_BUCKET_DEV }}
stage_name: development
secrets:
aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID_MBU }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY_MBU }}
uat:
if: startsWith(github.event.ref, 'refs/heads/main')
uses: ./.github/workflows/pipeline_template.yaml
needs: [development]
with:
sam_deploy_overrides: "StageName=uat LogLevel=DEBUG"
stack_name: ${{ vars.CLIENTS_STACK_NAME }}-uat
sam_template: src/clients/template.yaml
aws_region: us-east-1
pipeline_execution_role: ${{ vars.PIPELINE_EXECUTION_ROLE_UAT }}
cloudformation_execution_role: ${{ vars.CLOUDFORMATION_EXECUTION_ROLE_UAT }}
artifacts_bucket: ${{ vars.ARTIFACTS_BUCKET_UAT }}
stage_name: uat
secrets:
aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID_HUAT }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY_HUAT }}
Empty file added src/clients/__init__.py
Empty file.
Empty file.
30 changes: 30 additions & 0 deletions src/clients/client_modules/dao/client_dao.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Own's modules
from client_modules.data_access.dynamo_handler import DynamoDBHandler
import settings


class ClientDAO:
"""
A class for handling interactions with the DynamoDB table and the Lambda Function.
"""

def __init__(self):
"""
Initializes a new instance of the DAO class.
"""
self.clients_db = DynamoDBHandler(
table_name=settings.CLIENTS_TABLE_NAME,
partition_key="Id",
)

def create_client(self, item: dict) -> dict:
"""
Attempts to insert a new record for a client into the DynamoDB table.
:param item: Client representation
:type item: dict
:return: a dictionary that contains the response object
:rtype: dict
"""
response = self.clients_db.insert_record(item)
return response
Empty file.
245 changes: 245 additions & 0 deletions src/clients/client_modules/data_access/dynamo_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
# Python libraries
import json
from decimal import Decimal
from typing import Dict
from typing import Any

# Own modules
from client_modules.utils.aws import AWSClientManager

# Third-party libraries
from aws_lambda_powertools import Logger
from botocore.exceptions import ClientError


class DynamoDBHandler:
"""
A class for handling interactions with a DynamoDB table
"""

HTTP_STATUS_OK = 200
HTTP_STATUS_CREATED = 201
HTTP_STATUS_NO_CONTENT = 204
HTTP_STATUS_BAD_REQUEST = 400
HTTP_STATUS_FORBIDDEN = 403
HTTP_STATUS_NOT_FOUND = 404
HTTP_STATUS_INTERNAL_SERVER_ERROR = 500

def __init__(self, table_name: str, partition_key: str, sort_key: str = None):
self.table_name = table_name
self.partition_key = partition_key
self.sort_key = sort_key
aws_resources_manager = AWSClientManager()
dynamodb_resource = aws_resources_manager.dynamodb
self.table = dynamodb_resource.Table(table_name)
self.logger = Logger()

def insert_record(self, item: dict) -> Dict[str, Any]:
"""This function is used to save a record to a database.
It takes in a dictionary, which is build from a Client Model, as an argument and attempts to put the item into the database.
If the response from the database is successful, it returns a status of "success".
If not, it returns a status of "error" along with the HTTP status code and details about the error message.
If there is an AWS ClientError, it logs information about the error and also returns a status of "error" along
with the HTTP status code and details about the error message.
:param item: client representation built as a dict
:type item: dict
:return: A summary of the put_item action
:rtype: Dict[str, Any]
"""
try:
db_item = json.loads(json.dumps(item), parse_float=Decimal)
response = self.table.put_item(Item=db_item)
if response["ResponseMetadata"]["HTTPStatusCode"] == self.HTTP_STATUS_OK:
self.logger.info("Client was created in DynamoDB")
return self.build_response_object(
status="success",
status_code=self.HTTP_STATUS_CREATED,
message="Record saved in DynamoDB",
)
else:
message = response["Error"]["Message"]
self.logger.error(f"Failed saving record: Details: {message}")
return self.build_response_object(
status="error",
status_code=response["ResponseMetadata"]["HTTPStatusCode"],
message=message,
)
except ClientError as error:
message = f"{error.response['Error']['Message']}. {error.response['Error']['Code']}"
self.logger.error(f"ClientError when saving record: Details: {message}")
return self.build_response_object(
status="error",
status_code=error.response["ResponseMetadata"]["HTTPStatusCode"],
message=message,
)
except Exception as error:
self.logger.error(f"Exception when saving record: Details: {error}")
return self.build_response_object(
status="error",
status_code=self.HTTP_STATUS_INTERNAL_SERVER_ERROR,
message=str(error),
)

def scan_table(self) -> Dict[str, Any]:
"""This function is used to fetch all the records in the table.
If the response from the database is successful, it returns a status of "success".
If not, it returns a status of "error" along with the HTTP status code and details about the error message.
If there is an AWS ClientError, it logs information about the error and also returns a status of "error" along
with the HTTP status code and details about the error message.
:return: A summary of the put_item action
:rtype: Dict[str, Any]
"""
try:
response = self.table.scan()
if response["ResponseMetadata"]["HTTPStatusCode"] == self.HTTP_STATUS_OK:
self.logger.info("Clients were retrieved from DynamoDB")
return self.build_response_object(
status="success",
status_code=self.HTTP_STATUS_OK,
message=f"{len(response['Items'])} were found in DynamoDB Clients table",
payload=response["Items"],
)
else:
message = response["Error"]["Message"]
self.logger.error(f"Failed saving record: Details: {message}")
return self.build_response_object(
status="error",
status_code=response["ResponseMetadata"]["HTTPStatusCode"],
message=message,
)
except ClientError as error:
message = f"{error.response['Error']['Message']}. {error.response['Error']['Code']}"
self.logger.error(f"ClientError when saving record: Details: {message}")
return self.build_response_object(
status="error",
status_code=error.response["ResponseMetadata"]["HTTPStatusCode"],
message=message,
)
except Exception as error:
self.logger.error(f"Exception when saving record: Details: {error}")
return self.build_response_object(
status="error",
status_code=self.HTTP_STATUS_INTERNAL_SERVER_ERROR,
message=str(error),
)

def delete_record(self, key: dict) -> Dict[str, Any]:
"""
This function is used to delete a record from the DynamoDB table.
It takes in a key dictionary as an argument and attempts to delete the item from the database.
If the response from the database is successful, it returns a status of "success".
If there is an AWS ClientError, it logs information about the error and also returns a status of "error" along
with the HTTP status code and details about the error message.
:param key: The key of the item to be deleted
:type key: dict
:return: A summary of the delete_item action
:rtype: Dict[str, Any]
"""
try:
response = self.table.delete_item(Key=key)
if response["ResponseMetadata"]["HTTPStatusCode"] == self.HTTP_STATUS_OK:
self.logger.info(f"Client with key {key} was deleted from DynamoDB")
return self.build_response_object(
status="success",
status_code=self.HTTP_STATUS_OK,
message="Record deleted from DynamoDB",
)
else:
message = response["Error"]["Message"]
self.logger.error(f"Failed deleting record: Details: {message}")
return self.build_response_object(
status="error",
status_code=response["ResponseMetadata"]["HTTPStatusCode"],
message=message,
)
except ClientError as error:
message = f"{error.response['Error']['Message']}. {error.response['Error']['Code']}"
self.logger.error(f"ClientError when deleting record: Details: {message}")
return self.build_response_object(
status="error",
status_code=error.response["ResponseMetadata"]["HTTPStatusCode"],
message=message,
)
except Exception as error:
self.logger.error(f"Exception when deleting record: Details: {error}")
return self.build_response_object(
status="error",
status_code=self.HTTP_STATUS_INTERNAL_SERVER_ERROR,
message=str(error),
)

def update_record(self, item: dict) -> Dict[str, Any]:
"""
This function is used to update a record in the database using put_item.
If the item already exists, it will be updated.
:param item: Item as dict
:type item: dict
:return: A summary of the put_item action
:rtype: Dict[str, Any]
"""
try:
db_item = json.loads(json.dumps(item), parse_float=Decimal)
response = self.table.put_item(Item=db_item)
if response["ResponseMetadata"]["HTTPStatusCode"] == self.HTTP_STATUS_OK:
self.logger.info("Client was updated in DynamoDB")
return self.build_response_object(
status="success",
status_code=self.HTTP_STATUS_OK,
message="Record updated in DynamoDB",
)
else:
message = response["Error"]["Message"]
self.logger.error(f"Failed updating record: Details: {message}")
return self.build_response_object(
status="error",
status_code=response["ResponseMetadata"]["HTTPStatusCode"],
message=message,
)
except ClientError as error:
message = f"{error.response['Error']['Message']}. {error.response['Error']['Code']}"
self.logger.error(f"ClientError when updating record: Details: {message}")
return self.build_response_object(
status="error",
status_code=error.response["ResponseMetadata"]["HTTPStatusCode"],
message=message,
)
except Exception as error:
self.logger.error(f"Exception when updating record: Details: {error}")
return self.build_response_object(
status="error",
status_code=self.HTTP_STATUS_INTERNAL_SERVER_ERROR,
message=str(error),
)

def build_response_object(
self,
status: str,
status_code: int,
message: str,
payload: Dict[str, Any] = None,
) -> Dict[str, Any]:
"""
This method maps an status code a message into the response dictionary
:param status: Success or Error
:type status: str
:param status_code: Http Status Code
:type status_code: int
:param message: A string that contains the message to be returned
:type error_message: str
:param payload: Object with data from DynamoDb
:type error_message: dict
:return: a dictionary with the message
:rtype: Dict[str, Any]
"""

return {
"status": status,
"status_code": status_code,
"message": message,
"payload": payload,
}
54 changes: 54 additions & 0 deletions src/clients/client_modules/data_access/geolocation_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Python's libraries
from typing import Dict

# Own's modules
from client_modules.utils.aws import AWSClientManager
import settings

# Third-party libraries
from aws_lambda_powertools import Logger


class Geolocation:

def get_lat_and_long_from_street_address(
self, str_address: str
) -> Dict[str, float]:
"""This function will check AWS Locatio Service to match an address with a geolocation coordinates
:param str_address: String representation of the address received by client inside the inputs payload
:type str_address: str
"""
logger = Logger()
if settings.environment != "local":
aws_resources = AWSClientManager()
location = None
if isinstance(str_address, str):
try:
aws_repsonse = aws_resources.location.search_place_index_for_text(
IndexName="HiBerrySearchLocationIndex", Text=str_address
)
if aws_repsonse["ResponseMetadata"]["HTTPStatusCode"] == 200:
logger.info(f"Location: {str_address} found")
lat = aws_repsonse["Results"][0]["Place"]["Geometry"]["Point"][
1
]
long = aws_repsonse["Results"][0]["Place"]["Geometry"]["Point"][
0
]
location = {"latitude": lat, "longitude": long}
else:
logger.warning("AWS Response was not successfull")

except Exception as e:
logger.warning(
f"Something failed while fetching data from AWS. Details {e}"
)
else:
logger.warning("Input provided was not a string")

return location

else:
return {"latitude": 20.721722843875, "longitude": -103.370054309085}
Empty file.
Loading

0 comments on commit dc94c59

Please sign in to comment.