-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
enrique
committed
May 16, 2024
1 parent
f4017cc
commit dc94c59
Showing
23 changed files
with
1,063 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
245
src/clients/client_modules/data_access/dynamo_handler.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
54
src/clients/client_modules/data_access/geolocation_handler.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Oops, something went wrong.