Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding CreateClientFunction #50

Closed
wants to merge 4 commits into from
Closed
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
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",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is partition key maped from phone to 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]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont thnk we are going to need this one

"""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
Loading