From 6257e33220e32913fd2adf3bbdac8436ee4f2528 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sun, 10 Sep 2023 15:58:34 -0400 Subject: [PATCH 01/37] feat: implement dynamodb resource --- .chalice/config.json | 22 +++++++++++++++------- Pipfile | 1 + Pipfile.lock | 23 ++++++++++++++++++++--- app.py | 43 +++++++++++++++++++++++-------------------- chalicelib/db.py | 15 +++++++++++++++ chalicelib/s3.py | 0 6 files changed, 74 insertions(+), 30 deletions(-) create mode 100644 chalicelib/db.py create mode 100644 chalicelib/s3.py diff --git a/.chalice/config.json b/.chalice/config.json index 5583a3c..189ac4d 100644 --- a/.chalice/config.json +++ b/.chalice/config.json @@ -1,10 +1,18 @@ { - "version": "2.0", - "app_name": "zap", - "stages": { - "dev": { - "api_gateway_stage": "api" - } + "version": "2.0", + "app_name": "tierpyo", + "stages": { + "dev": { + "environment_variables": { + "ENV": "dev" + }, + "api_gateway_stage": "api" + }, + "prod": { + "environment_variables": { + "ENV": "prod" + }, + "api_gateway_stage": "api" } } - \ No newline at end of file +} diff --git a/Pipfile b/Pipfile index 78b708d..f598864 100644 --- a/Pipfile +++ b/Pipfile @@ -5,6 +5,7 @@ name = "pypi" [packages] chalice = "*" +boto3 = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index f318fff..f1ea088 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b0ae9fbf30097a0f9ea4291fbd175c7497dfd4737562ada3802b08899322fd1a" + "sha256": "a80f1beb51a4626d695c43f75d239103b62f1fcacaac64c2e20e7230e4ef7ab5" }, "pipfile-spec": 6, "requires": { @@ -25,6 +25,15 @@ "markers": "python_version >= '2.7'", "version": "==1.20.0" }, + "boto3": { + "hashes": [ + "sha256:c53c92dfe22489ba31e918c2e7b59ff43e2e778bd3d3559e62351a739382bb5c", + "sha256:eea3b07e0f28c9f92bccab972af24a3b0dd951c69d93da75227b8ecd3e18f6c4" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.28.44" + }, "botocore": { "hashes": [ "sha256:83d61c1ca781e6ede19fcc4d5dd73004eee3825a2b220f0d7727e32069209d98", @@ -78,7 +87,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, "python-editor": { @@ -155,6 +164,14 @@ "markers": "python_version >= '3.7'", "version": "==4.0.5" }, + "s3transfer": { + "hashes": [ + "sha256:b014be3a8a2aab98cfe1abc7229cc5a9a0cf05eb9c1f2b86b230fd8df3f78084", + "sha256:cab66d3380cca3e70939ef2255d01cd8aece6a4907a9528740f668c4b0611861" + ], + "markers": "python_version >= '3.7'", + "version": "==0.6.2" + }, "setuptools": { "hashes": [ "sha256:00478ca80aeebeecb2f288d3206b0de568df5cd2b8fada1209843cc9a8d88a48", @@ -168,7 +185,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "typing-extensions": { diff --git a/app.py b/app.py index 33d5f68..84d269f 100644 --- a/app.py +++ b/app.py @@ -1,29 +1,32 @@ from chalice import Chalice +from chalicelib.db import DBResource +import uuid app = Chalice(app_name='test') - +db = DBResource() @app.route('/') def index(): return {'hello': 'world'} +@app.route("/submit", methods=["POST"]) +def submit_form(): + + # Get data as JSON and attach unique id for applicantId + data = app.current_request.json_body + applicant_id = str(uuid.uuid4()) + data["applicantId"] = applicant_id + + db.put_data("zap-applications", data) + + + return { + "msg": True + } + +""" +TODO: +Submit form data to dynamodb and mongodb +save resume in S3 -> generate link -# The view function above will return {"hello": "world"} -# whenever you make an HTTP GET request to '/'. -# -# Here are a few more examples: -# -# @app.route('/hello/{name}') -# def hello_name(name): -# # '/hello/james' -> {"hello": "james"} -# return {'hello': name} -# -# @app.route('/users', methods=['POST']) -# def create_user(): -# # This is the JSON body the user sent in their POST request. -# user_as_json = app.current_request.json_body -# # We'll echo the json body back to the user in a 'user' key. -# return {'user': user_as_json} -# -# See the README documentation for more examples. -# +""" \ No newline at end of file diff --git a/chalicelib/db.py b/chalicelib/db.py new file mode 100644 index 0000000..c088b4a --- /dev/null +++ b/chalicelib/db.py @@ -0,0 +1,15 @@ +import os +import boto3 + +class DBResource: + def __init__(self): + self.is_prod = os.environ.get("ENV") == "prod" + self.db = boto3.resource('dynamodb') + + def put_data(self, table_name: str, data): + if self.is_prod: table_name += "-prod" + else: table_name += "-dev" + table = self.db.Table(table_name) + table.put_item( + Item=data + ) \ No newline at end of file diff --git a/chalicelib/s3.py b/chalicelib/s3.py new file mode 100644 index 0000000..e69de29 From 2a9770f98ad7cbbeb33e3ace95a7fcd9215ebc95 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sun, 10 Sep 2023 16:01:39 -0400 Subject: [PATCH 02/37] feat: initialize s3 client --- .chalice/config.json | 2 +- chalicelib/s3.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.chalice/config.json b/.chalice/config.json index 189ac4d..9885423 100644 --- a/.chalice/config.json +++ b/.chalice/config.json @@ -1,6 +1,6 @@ { "version": "2.0", - "app_name": "tierpyo", + "app_name": "zap", "stages": { "dev": { "environment_variables": { diff --git a/chalicelib/s3.py b/chalicelib/s3.py index e69de29..9d86d52 100644 --- a/chalicelib/s3.py +++ b/chalicelib/s3.py @@ -0,0 +1,23 @@ +import boto3 +import os +import json +from datetime import datetime + +class S3Client(): + def __init__(self, chalice_app): + self.chalice_app = chalice_app + self.bucket_name = "whyphi-zap" + self.is_prod = os.environ.get("ENV") == "prod" + self.s3 = boto3.client('s3') + + def upload_file_to_json(self, path: str, content: dict): + """Converts python dictionary into JSON and uploads JSON file to S3 Bucket + """ + # Set path + if self.is_prod: + path = f"prod/{path}" + else: + path = f"dev/{path}" + + try: + pass \ No newline at end of file From 2d04d116bfc7c96b7e6f15cc75b665a7be612944 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sun, 10 Sep 2023 16:08:17 -0400 Subject: [PATCH 03/37] chore: add deployed directory --- .chalice/deployed/dev.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .chalice/deployed/dev.json diff --git a/.chalice/deployed/dev.json b/.chalice/deployed/dev.json new file mode 100644 index 0000000..2afa883 --- /dev/null +++ b/.chalice/deployed/dev.json @@ -0,0 +1,23 @@ +{ + "resources": [ + { + "name": "default-role", + "resource_type": "iam_role", + "role_arn": "arn:aws:iam::280776660572:role/zap-dev", + "role_name": "zap-dev" + }, + { + "name": "api_handler", + "resource_type": "lambda_function", + "lambda_arn": "arn:aws:lambda:us-east-1:280776660572:function:zap-dev" + }, + { + "name": "rest_api", + "resource_type": "rest_api", + "rest_api_id": "zai9h1x1ze", + "rest_api_url": "https://zai9h1x1ze.execute-api.us-east-1.amazonaws.com/api/" + } + ], + "schema_version": "2.0", + "backend": "api" +} From 414e35bbdc9b3584a116ad014eae727b17d55587 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sun, 10 Sep 2023 16:10:20 -0400 Subject: [PATCH 04/37] chore: create chalice prod deployment package --- .chalice/deployed/prod.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .chalice/deployed/prod.json diff --git a/.chalice/deployed/prod.json b/.chalice/deployed/prod.json new file mode 100644 index 0000000..94e18d1 --- /dev/null +++ b/.chalice/deployed/prod.json @@ -0,0 +1,23 @@ +{ + "resources": [ + { + "name": "default-role", + "resource_type": "iam_role", + "role_arn": "arn:aws:iam::280776660572:role/zap-prod", + "role_name": "zap-prod" + }, + { + "name": "api_handler", + "resource_type": "lambda_function", + "lambda_arn": "arn:aws:lambda:us-east-1:280776660572:function:zap-prod" + }, + { + "name": "rest_api", + "resource_type": "rest_api", + "rest_api_id": "iszokcpzjb", + "rest_api_url": "https://iszokcpzjb.execute-api.us-east-1.amazonaws.com/api/" + } + ], + "schema_version": "2.0", + "backend": "api" +} From dc80ccf4197b95565815533f967774569b683f2e Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sun, 10 Sep 2023 16:26:28 -0400 Subject: [PATCH 05/37] feat: implement resume upload functionality --- .gitignore | 5 ++++- app.py | 26 ++++++++++++++++---------- chalicelib/s3.py | 30 +++++++++++++++++++++--------- chalicelib/utils.py | 7 +++++++ 4 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 chalicelib/utils.py diff --git a/.gitignore b/.gitignore index 40359c5..ce43805 100644 --- a/.gitignore +++ b/.gitignore @@ -163,4 +163,7 @@ cython_debug/ .chalice/venv/ .DS_Store -*.DS_Store \ No newline at end of file +*.DS_Store + +.vscode +.vscode/* \ No newline at end of file diff --git a/app.py b/app.py index 84d269f..86efcf0 100644 --- a/app.py +++ b/app.py @@ -1,32 +1,38 @@ from chalice import Chalice from chalicelib.db import DBResource +from chalicelib.s3 import S3Client import uuid -app = Chalice(app_name='test') +app = Chalice(app_name="zap") db = DBResource() +s3 = S3Client() -@app.route('/') + +@app.route("/") def index(): - return {'hello': 'world'} + return {"hello": "world"} + @app.route("/submit", methods=["POST"]) def submit_form(): - # Get data as JSON and attach unique id for applicantId data = app.current_request.json_body applicant_id = str(uuid.uuid4()) data["applicantId"] = applicant_id - + + # Upload resume and retrieve, then set link to data + resume_path = f"resume/{data['lastName']}_{data['firstName']}_{applicant_id}.pdf" + resume_url = s3.upload_resume(resume_path, data["resume"]) + data["resume"] = resume_url + db.put_data("zap-applications", data) - - return { - "msg": True - } + return {"msg": True, "resumeUrl": resume_url} + """ TODO: Submit form data to dynamodb and mongodb save resume in S3 -> generate link -""" \ No newline at end of file +""" diff --git a/chalicelib/s3.py b/chalicelib/s3.py index 9d86d52..10a2441 100644 --- a/chalicelib/s3.py +++ b/chalicelib/s3.py @@ -2,22 +2,34 @@ import os import json from datetime import datetime +from chalicelib.utils import decode_base64 -class S3Client(): - def __init__(self, chalice_app): - self.chalice_app = chalice_app + +class S3Client: + def __init__(self): self.bucket_name = "whyphi-zap" self.is_prod = os.environ.get("ENV") == "prod" - self.s3 = boto3.client('s3') + self.s3 = boto3.client("s3") - def upload_file_to_json(self, path: str, content: dict): - """Converts python dictionary into JSON and uploads JSON file to S3 Bucket - """ + def upload_resume(self, path: str, data) -> str: + """Uploads resume to S3 Bucket and returns path""" # Set path if self.is_prod: path = f"prod/{path}" else: path = f"dev/{path}" - try: - pass \ No newline at end of file + metadata = { + "Content-Type": "application/pdf", + } + + binary_data = decode_base64(data) + + self.s3.put_object( + Bucket=self.bucket_name, Key=path, Body=binary_data, Metadata=metadata + ) + + s3_endpoint = f"https://{self.bucket_name}.s3.amazonaws.com/" + resume_url = s3_endpoint + path + + return resume_url diff --git a/chalicelib/utils.py b/chalicelib/utils.py new file mode 100644 index 0000000..b740cf3 --- /dev/null +++ b/chalicelib/utils.py @@ -0,0 +1,7 @@ +import base64 + + +def decode_base64(base64_data): + """Decodes base64 data into binary data.""" + binary_data = base64.b64decode(base64_data) + return binary_data From c48ec18b82cf80abf05b18d3bfcac6e17f6c26e5 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sun, 10 Sep 2023 16:38:01 -0400 Subject: [PATCH 06/37] feat: implement image upload to s3 functionality --- app.py | 13 +++++++++++-- chalicelib/s3.py | 2 +- chalicelib/utils.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index 86efcf0..fe1df36 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,8 @@ from chalice import Chalice from chalicelib.db import DBResource from chalicelib.s3 import S3Client +from chalicelib.utils import get_image_extension_from_base64 + import uuid app = Chalice(app_name="zap") @@ -22,8 +24,15 @@ def submit_form(): # Upload resume and retrieve, then set link to data resume_path = f"resume/{data['lastName']}_{data['firstName']}_{applicant_id}.pdf" - resume_url = s3.upload_resume(resume_path, data["resume"]) - data["resume"] = resume_url + resume_url = s3.upload_binary_data(resume_path, data["resume"]) + + # Upload photo and retrieve, then set link to data + image_extension = get_image_extension_from_base64(data["image"]) + image_path = f"image/{data['lastName']}_{data['firstName']}_{applicant_id}.{image_extension}" + image_url = s3.upload_binary_data(image_path, data["image"]) + + + data["resume"], data["image"] = resume_url, image_url db.put_data("zap-applications", data) diff --git a/chalicelib/s3.py b/chalicelib/s3.py index 10a2441..bedb017 100644 --- a/chalicelib/s3.py +++ b/chalicelib/s3.py @@ -11,7 +11,7 @@ def __init__(self): self.is_prod = os.environ.get("ENV") == "prod" self.s3 = boto3.client("s3") - def upload_resume(self, path: str, data) -> str: + def upload_binary_data(self, path: str, data) -> str: """Uploads resume to S3 Bucket and returns path""" # Set path if self.is_prod: diff --git a/chalicelib/utils.py b/chalicelib/utils.py index b740cf3..3bdf450 100644 --- a/chalicelib/utils.py +++ b/chalicelib/utils.py @@ -1,7 +1,39 @@ import base64 +import imghdr def decode_base64(base64_data): """Decodes base64 data into binary data.""" binary_data = base64.b64decode(base64_data) return binary_data + +def get_image_extension_from_base64(base64_data): + try: + # Decode the Base64 data to bytes + binary_data = base64.b64decode(base64_data) + + # Use imghdr to identify the image format + image_format = imghdr.what(None, binary_data) + + if image_format: + # Normalize the format to lowercase (e.g., 'JPEG' -> 'jpg') + image_format = image_format.lower() + + # You can return the image format or the corresponding file extension + # If you want the extension, you can map known formats to extensions + format_to_extension = { + 'jpeg': 'jpg', # 'jpeg' is returned by imghdr for JPEG files + 'png': 'png', + 'heic': 'heic', + # Add more formats as needed + } + + extension = format_to_extension.get(image_format) + return extension + + except Exception as e: + # Handle decoding or identification errors here + print(f"Error: {str(e)}") + + # Return None if the format cannot be determined + return None \ No newline at end of file From 7876b0ae6afdde70d2a6b8fa3ca57b391e8bcb12 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sun, 10 Sep 2023 17:11:55 -0400 Subject: [PATCH 07/37] feat: implement get all applicants API --- app.py | 14 +++++++++++--- chalicelib/db.py | 33 ++++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/app.py b/app.py index fe1df36..e441fef 100644 --- a/app.py +++ b/app.py @@ -28,16 +28,24 @@ def submit_form(): # Upload photo and retrieve, then set link to data image_extension = get_image_extension_from_base64(data["image"]) - image_path = f"image/{data['lastName']}_{data['firstName']}_{applicant_id}.{image_extension}" + image_path = ( + f"image/{data['lastName']}_{data['firstName']}_{applicant_id}.{image_extension}" + ) image_url = s3.upload_binary_data(image_path, data["image"]) - + # Reset data properties as S3 url data["resume"], data["image"] = resume_url, image_url - db.put_data("zap-applications", data) + # Upload data to DynamoDB + db.put_data(table_name="zap-applications", data=data) return {"msg": True, "resumeUrl": resume_url} +@app.route("/applicants", methods=["GET"]) +def get_applicants(): + data = db.get_all(table_name="zap-applications") + return data + """ TODO: diff --git a/chalicelib/db.py b/chalicelib/db.py index c088b4a..dc6bd0f 100644 --- a/chalicelib/db.py +++ b/chalicelib/db.py @@ -1,15 +1,34 @@ import os import boto3 + class DBResource: def __init__(self): self.is_prod = os.environ.get("ENV") == "prod" - self.db = boto3.resource('dynamodb') - + self.db = boto3.resource("dynamodb") + def put_data(self, table_name: str, data): - if self.is_prod: table_name += "-prod" - else: table_name += "-dev" + if self.is_prod: + table_name += "-prod" + else: + table_name += "-dev" + + table = self.db.Table(table_name) + table.put_item(Item=data) + + + def get_all(self, table_name: str): + if self.is_prod: + table_name += "-prod" + else: + table_name += "-dev" + table = self.db.Table(table_name) - table.put_item( - Item=data - ) \ No newline at end of file + + # Use the scan operation to retrieve all items from the table + response = table.scan() + + # The response contains the items as well as other information like metadata + items = response.get("Items", []) + + return items \ No newline at end of file From b53faa83c592665c726179a7c3eaebb930144d85 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sun, 10 Sep 2023 17:20:35 -0400 Subject: [PATCH 08/37] docs: add --- README.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..5748f3f --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Zap + +Zap is whyphi's temporary (maybe permanent) serverless API solution. + +Zap is created using: +- AWS Chalice: Framework that abstracts Python code as serverless functions +- AWS Lambda +- AWS API Gateway +- AWS DynamoDB: AWS's NoSQL Database +- AWS S3: Handles data for application +- AWS IAM: Managing permissions and policies within AWS services + +## To get started + +Ensure that you have [AWS CLI](https://aws.amazon.com/cli/) installed. Then, set the necessary AWS configuration within your system using: + +```bash +aws configure +``` + +## Local Development + +Within Zap, Python dependences are managed using [`pipenv`](https://pipenv.pypa.io/en/latest/). Ensure you have `pipenv` installed within your machine. + +To turn on the virtual environment using `pipenv`: +```bash +pipenv shell +``` + +To install necessary dependencies within `pipenv`: +```bash +pipenv shell +``` + +To install any additional dependenceis within `pipenv`: +```bash +pipenv install {dependency name} +``` + +To enable local server for Chalice: +```bash +chalice local +``` + +## Deployment + +### PR Strategy + +PRs should be made in the following order: + + Personal PR -> `dev/*` -> `staging` -> `prod` + +### CI/CD + +This work exists to minimize potential errors being pushed to production. All deployments will happen when PRs are pushed to `dev`, `staging`, and `prod` automatically as GitHub Actions has been already setup. + +To find `dev` and `prod` API endpoints, access AWS Lambda and find the respective API Gateway link. \ No newline at end of file From 2261d3c7f4806180d01bc82e4880f40742c2da49 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Tue, 12 Sep 2023 22:47:04 -0400 Subject: [PATCH 09/37] feat: create very simple create listing API --- app.py | 12 ++++++++++++ chalicelib/db.py | 37 +++++++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/app.py b/app.py index e441fef..3557b1b 100644 --- a/app.py +++ b/app.py @@ -46,6 +46,18 @@ def get_applicants(): data = db.get_all(table_name="zap-applications") return data +@app.route("/create", methods=["POST"]) +def create_listing(): + """Creates a new listing with given information + """ + data = app.current_request.json_body + listing_id = str(uuid.uuid4()) + data["listingId"] = listing_id + + db.put_data(table_name="zap-listings", data=data) + + return {"msg": True} + """ TODO: diff --git a/chalicelib/db.py b/chalicelib/db.py index dc6bd0f..1127838 100644 --- a/chalicelib/db.py +++ b/chalicelib/db.py @@ -1,11 +1,36 @@ import os import boto3 +from botocore import errorfactory class DBResource: def __init__(self): self.is_prod = os.environ.get("ENV") == "prod" - self.db = boto3.resource("dynamodb") + self.resource = boto3.resource("dynamodb") + self.primary_keys = {"zap-listings": "listingId"} + + # def create_table(self, table_name: str, primary_key: str): + # """Creates a table with a given primary_key""" + # table = self.resource.create_table( + # TableName=table_name, + # KeySchema=[ + # { + # "AttributeName": primary_key, + # "KeyType": "HASH", # Assuming you have a hash key + # } + # ], + # AttributeDefinitions=[ + # { + # "AttributeName": primary_key, + # "AttributeType": "S", # Assuming it's a string + # } + # ], + # ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + # ) + # # Wait until the table exists + # table.meta.client.get_waiter("table_exists").wait(TableName=table_name) + # print(table) + # return table def put_data(self, table_name: str, data): if self.is_prod: @@ -13,22 +38,22 @@ def put_data(self, table_name: str, data): else: table_name += "-dev" - table = self.db.Table(table_name) + table = self.resource.Table(table_name) + table.put_item(Item=data) - def get_all(self, table_name: str): if self.is_prod: table_name += "-prod" else: table_name += "-dev" - table = self.db.Table(table_name) - + table = self.resource.Table(table_name) + # Use the scan operation to retrieve all items from the table response = table.scan() # The response contains the items as well as other information like metadata items = response.get("Items", []) - return items \ No newline at end of file + return items From 3a42d15eee444f52a944c24f8e4d393ed32119f5 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Tue, 12 Sep 2023 22:48:40 -0400 Subject: [PATCH 10/37] feat: create simple get all listings API --- app.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app.py b/app.py index 3557b1b..a68fa43 100644 --- a/app.py +++ b/app.py @@ -58,6 +58,14 @@ def create_listing(): return {"msg": True} +@app.route("/listings", methods=["GET"]) +def get_all_listings(): + """Gets all listings available + """ + data = db.get_all(table_name="zap-listings") + return data + + """ TODO: From c51308b3330bbf4e3bdfae0eacb4d576ab755a40 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Tue, 12 Sep 2023 23:16:41 -0400 Subject: [PATCH 11/37] feat: implement cors for API endpoints --- app.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/app.py b/app.py index a68fa43..fc82c51 100644 --- a/app.py +++ b/app.py @@ -15,7 +15,7 @@ def index(): return {"hello": "world"} -@app.route("/submit", methods=["POST"]) +@app.route("/submit", methods=["POST"], cors=True) def submit_form(): # Get data as JSON and attach unique id for applicantId data = app.current_request.json_body @@ -41,15 +41,16 @@ def submit_form(): return {"msg": True, "resumeUrl": resume_url} -@app.route("/applicants", methods=["GET"]) + +@app.route("/applicants", methods=["GET"], cors=True) def get_applicants(): data = db.get_all(table_name="zap-applications") return data -@app.route("/create", methods=["POST"]) + +@app.route("/create", methods=["POST"], cors=True) def create_listing(): - """Creates a new listing with given information - """ + """Creates a new listing with given information""" data = app.current_request.json_body listing_id = str(uuid.uuid4()) data["listingId"] = listing_id @@ -58,18 +59,9 @@ def create_listing(): return {"msg": True} -@app.route("/listings", methods=["GET"]) + +@app.route("/listings", methods=["GET"], cors=True) def get_all_listings(): - """Gets all listings available - """ + """Gets all listings available""" data = db.get_all(table_name="zap-listings") return data - - - -""" -TODO: -Submit form data to dynamodb and mongodb -save resume in S3 -> generate link - -""" From 9559d6fdc88caa96f4cb73eea2d4659053edbc72 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Wed, 13 Sep 2023 08:13:41 -0400 Subject: [PATCH 12/37] refactor: use decorate to add env suffix to table_name --- chalicelib/db.py | 47 ++++++++++++----------------------------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/chalicelib/db.py b/chalicelib/db.py index 1127838..702a865 100644 --- a/chalicelib/db.py +++ b/chalicelib/db.py @@ -9,50 +9,27 @@ def __init__(self): self.resource = boto3.resource("dynamodb") self.primary_keys = {"zap-listings": "listingId"} - # def create_table(self, table_name: str, primary_key: str): - # """Creates a table with a given primary_key""" - # table = self.resource.create_table( - # TableName=table_name, - # KeySchema=[ - # { - # "AttributeName": primary_key, - # "KeyType": "HASH", # Assuming you have a hash key - # } - # ], - # AttributeDefinitions=[ - # { - # "AttributeName": primary_key, - # "AttributeType": "S", # Assuming it's a string - # } - # ], - # ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, - # ) - # # Wait until the table exists - # table.meta.client.get_waiter("table_exists").wait(TableName=table_name) - # print(table) - # return table + def add_env_suffix(func): + def wrapper(self, table_name: str, *args, **kwargs): + if self.is_prod: + table_name += "-prod" + else: + table_name += "-dev" - def put_data(self, table_name: str, data): - if self.is_prod: - table_name += "-prod" - else: - table_name += "-dev" + return func(self, table_name, *args, **kwargs) - table = self.resource.Table(table_name) + return wrapper + @add_env_suffix + def put_data(self, table_name: str, data): + table = self.resource.Table(table_name) table.put_item(Item=data) + @add_env_suffix def get_all(self, table_name: str): - if self.is_prod: - table_name += "-prod" - else: - table_name += "-dev" - table = self.resource.Table(table_name) - # Use the scan operation to retrieve all items from the table response = table.scan() - # The response contains the items as well as other information like metadata items = response.get("Items", []) From 7ae7ece79338fbfba3fe8bf3bd722847df089eb7 Mon Sep 17 00:00:00 2001 From: Taesung Yoon Date: Wed, 13 Sep 2023 21:26:33 -0400 Subject: [PATCH 13/37] fix: remove full version python on pipfile --- Pipfile | 1 - Pipfile.lock | 29 ++++++----------------------- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/Pipfile b/Pipfile index 78b708d..e43344d 100644 --- a/Pipfile +++ b/Pipfile @@ -10,4 +10,3 @@ chalice = "*" [requires] python_version = "3.9" -python_full_version = "3.9.6" diff --git a/Pipfile.lock b/Pipfile.lock index f318fff..9dd429c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,10 @@ { "_meta": { "hash": { - "sha256": "b0ae9fbf30097a0f9ea4291fbd175c7497dfd4737562ada3802b08899322fd1a" + "sha256": "efc53bf590c5d7898408f63b08a384d064a1000669e752018a3a217380a8f86a" }, "pipfile-spec": 6, "requires": { - "python_full_version": "3.9.6", "python_version": "3.9" }, "sources": [ @@ -27,11 +26,11 @@ }, "botocore": { "hashes": [ - "sha256:83d61c1ca781e6ede19fcc4d5dd73004eee3825a2b220f0d7727e32069209d98", - "sha256:84f90919fecb4a4f417fd10145c8a87ff2c4b14d6381cd34d9babf02110b3315" + "sha256:6a60f9601270458102529b17fdcba5551b918f9eedc32bbc2f467e63edfb2662", + "sha256:a0ba5629eb17a37bf449bccda9df6ae652d5755f73145519d5eb244f6963b31b" ], "markers": "python_version >= '3.7'", - "version": "==1.31.44" + "version": "==1.31.47" }, "chalice": { "hashes": [ @@ -65,20 +64,12 @@ "markers": "python_version >= '3.7'", "version": "==1.0.1" }, - "pip": { - "hashes": [ - "sha256:0e7c86f486935893c708287b30bd050a36ac827ec7fe5e43fe7cb198dd835fba", - "sha256:3ef6ac33239e4027d9a5598a381b9d30880a1477e50039db2eac6e8a8f6d1b18" - ], - "markers": "python_version >= '3.7'", - "version": "==23.1.2" - }, "python-dateutil": { "hashes": [ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, "python-editor": { @@ -155,20 +146,12 @@ "markers": "python_version >= '3.7'", "version": "==4.0.5" }, - "setuptools": { - "hashes": [ - "sha256:00478ca80aeebeecb2f288d3206b0de568df5cd2b8fada1209843cc9a8d88a48", - "sha256:af3d5949030c3f493f550876b2fd1dd5ec66689c4ee5d5344f009746f71fd5a8" - ], - "markers": "python_version >= '3.8'", - "version": "==68.2.0" - }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "typing-extensions": { From 034c24698a5307bd2323681b13076a46a1bbc5bc Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Wed, 13 Sep 2023 21:28:54 -0400 Subject: [PATCH 14/37] fix: fix pipfile --- Pipfile | 1 - Pipfile.lock | 21 ++++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Pipfile b/Pipfile index f598864..a896cec 100644 --- a/Pipfile +++ b/Pipfile @@ -11,4 +11,3 @@ boto3 = "*" [requires] python_version = "3.9" -python_full_version = "3.9.6" diff --git a/Pipfile.lock b/Pipfile.lock index f1ea088..d61e98b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,10 @@ { "_meta": { "hash": { - "sha256": "a80f1beb51a4626d695c43f75d239103b62f1fcacaac64c2e20e7230e4ef7ab5" + "sha256": "c86eb6d8aefdbf3562d91e701f45ef740a2bc86ba6a28aa6b027ef163c8ce038" }, "pipfile-spec": 6, "requires": { - "python_full_version": "3.9.6", "python_version": "3.9" }, "sources": [ @@ -27,20 +26,20 @@ }, "boto3": { "hashes": [ - "sha256:c53c92dfe22489ba31e918c2e7b59ff43e2e778bd3d3559e62351a739382bb5c", - "sha256:eea3b07e0f28c9f92bccab972af24a3b0dd951c69d93da75227b8ecd3e18f6c4" + "sha256:27560da44099e7e2ee961d3971d8ea659de2e0dc24e78043d1c3027d89b2d8a2", + "sha256:be69cd28e3732b63ad61f6d2429b1eac92428588911a5a7367faa4e129a4738d" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.28.44" + "version": "==1.28.47" }, "botocore": { "hashes": [ - "sha256:83d61c1ca781e6ede19fcc4d5dd73004eee3825a2b220f0d7727e32069209d98", - "sha256:84f90919fecb4a4f417fd10145c8a87ff2c4b14d6381cd34d9babf02110b3315" + "sha256:6a60f9601270458102529b17fdcba5551b918f9eedc32bbc2f467e63edfb2662", + "sha256:a0ba5629eb17a37bf449bccda9df6ae652d5755f73145519d5eb244f6963b31b" ], "markers": "python_version >= '3.7'", - "version": "==1.31.44" + "version": "==1.31.47" }, "chalice": { "hashes": [ @@ -174,11 +173,11 @@ }, "setuptools": { "hashes": [ - "sha256:00478ca80aeebeecb2f288d3206b0de568df5cd2b8fada1209843cc9a8d88a48", - "sha256:af3d5949030c3f493f550876b2fd1dd5ec66689c4ee5d5344f009746f71fd5a8" + "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87", + "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a" ], "markers": "python_version >= '3.8'", - "version": "==68.2.0" + "version": "==68.2.2" }, "six": { "hashes": [ From 77339de82da512b62aa612db2dbe31797cf2c896 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Wed, 13 Sep 2023 21:32:28 -0400 Subject: [PATCH 15/37] chore: pipfile recreate --- Pipfile.lock | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 0504ba4..16031de 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -73,12 +73,20 @@ "markers": "python_version >= '3.7'", "version": "==1.0.1" }, + "pip": { + "hashes": [ + "sha256:0e7c86f486935893c708287b30bd050a36ac827ec7fe5e43fe7cb198dd835fba", + "sha256:3ef6ac33239e4027d9a5598a381b9d30880a1477e50039db2eac6e8a8f6d1b18" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1.2" + }, "python-dateutil": { "hashes": [ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.2" }, "python-editor": { @@ -176,7 +184,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "typing-extensions": { From f231f5f80e6f63ae07b5372652e9d83585dcfdd5 Mon Sep 17 00:00:00 2001 From: Jin Young Bang <44107246+jinyoungbang@users.noreply.github.com> Date: Wed, 13 Sep 2023 21:44:12 -0400 Subject: [PATCH 16/37] docs: Update README.md --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5748f3f..4f1b66a 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,18 @@ Ensure that you have [AWS CLI](https://aws.amazon.com/cli/) installed. Then, set aws configure ``` +Ensure that you have Node.js [downloaded](https://nodejs.org/en/download) and your node version `v20.x.x`. + +To check your node version: +```bash +node -v +``` + +To update Node.js: +```bash +npm install -g n +``` + ## Local Development Within Zap, Python dependences are managed using [`pipenv`](https://pipenv.pypa.io/en/latest/). Ensure you have `pipenv` installed within your machine. @@ -54,4 +66,4 @@ PRs should be made in the following order: This work exists to minimize potential errors being pushed to production. All deployments will happen when PRs are pushed to `dev`, `staging`, and `prod` automatically as GitHub Actions has been already setup. -To find `dev` and `prod` API endpoints, access AWS Lambda and find the respective API Gateway link. \ No newline at end of file +To find `dev` and `prod` API endpoints, access AWS Lambda and find the respective API Gateway link. From 3661022bde415b0a7720b9bd416734c489ef503b Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Thu, 14 Sep 2023 16:05:49 -0400 Subject: [PATCH 17/37] feat: implement get listing API (high-level) --- app.py | 11 +++++++++++ chalicelib/db.py | 8 ++++++++ chalicelib/decorators.py | 10 ++++++++++ 3 files changed, 29 insertions(+) create mode 100644 chalicelib/decorators.py diff --git a/app.py b/app.py index fc82c51..2e52089 100644 --- a/app.py +++ b/app.py @@ -65,3 +65,14 @@ def get_all_listings(): """Gets all listings available""" data = db.get_all(table_name="zap-listings") return data + + +@app.route("/listings/{id}", methods=["GET"], cors=True) +def get_listing(id): + """Gets a listing from id + """ + data = db.get_item(table_name="zap-listings", key={ + "listingId": id + }) + + return data \ No newline at end of file diff --git a/chalicelib/db.py b/chalicelib/db.py index 702a865..725443d 100644 --- a/chalicelib/db.py +++ b/chalicelib/db.py @@ -34,3 +34,11 @@ def get_all(self, table_name: str): items = response.get("Items", []) return items + + @add_env_suffix + def get_item(self, table_name: str, key: dict): + """Gets an item from table_name through key specifier""" + table = self.resource.Table(table_name) + print(table) + item = table.get_item(Key=key) + return item diff --git a/chalicelib/decorators.py b/chalicelib/decorators.py new file mode 100644 index 0000000..65e92ad --- /dev/null +++ b/chalicelib/decorators.py @@ -0,0 +1,10 @@ +def add_env_suffix(func): + def wrapper(self, table_name: str, *args, **kwargs): + if "env" in kwargs and kwargs["env"]: + table_name += "-prod" + else: + table_name += "-dev" + + return func(self, table_name, *args, **kwargs) + + return wrapper \ No newline at end of file From e024e287555504dc0000f55c06915cd1c1911cf6 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Thu, 14 Sep 2023 19:33:35 -0400 Subject: [PATCH 18/37] fix: reimplement base64 parsing methods --- app.py | 4 ++-- chalicelib/s3.py | 7 +++--- chalicelib/utils.py | 52 +++++++++++++++++++-------------------------- 3 files changed, 28 insertions(+), 35 deletions(-) diff --git a/app.py b/app.py index 2e52089..b42e6c6 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,7 @@ from chalice import Chalice from chalicelib.db import DBResource from chalicelib.s3 import S3Client -from chalicelib.utils import get_image_extension_from_base64 +from chalicelib.utils import get_file_extension_from_base64 import uuid @@ -27,7 +27,7 @@ def submit_form(): resume_url = s3.upload_binary_data(resume_path, data["resume"]) # Upload photo and retrieve, then set link to data - image_extension = get_image_extension_from_base64(data["image"]) + image_extension = get_file_extension_from_base64(data["image"]) image_path = ( f"image/{data['lastName']}_{data['firstName']}_{applicant_id}.{image_extension}" ) diff --git a/chalicelib/s3.py b/chalicelib/s3.py index bedb017..ed48149 100644 --- a/chalicelib/s3.py +++ b/chalicelib/s3.py @@ -22,9 +22,10 @@ def upload_binary_data(self, path: str, data) -> str: metadata = { "Content-Type": "application/pdf", } - - binary_data = decode_base64(data) - + + base64_data = data.split(',')[1] + binary_data = decode_base64(base64_data) + self.s3.put_object( Bucket=self.bucket_name, Key=path, Body=binary_data, Metadata=metadata ) diff --git a/chalicelib/utils.py b/chalicelib/utils.py index 3bdf450..9dfab87 100644 --- a/chalicelib/utils.py +++ b/chalicelib/utils.py @@ -7,33 +7,25 @@ def decode_base64(base64_data): binary_data = base64.b64decode(base64_data) return binary_data -def get_image_extension_from_base64(base64_data): - try: - # Decode the Base64 data to bytes - binary_data = base64.b64decode(base64_data) - - # Use imghdr to identify the image format - image_format = imghdr.what(None, binary_data) - - if image_format: - # Normalize the format to lowercase (e.g., 'JPEG' -> 'jpg') - image_format = image_format.lower() - - # You can return the image format or the corresponding file extension - # If you want the extension, you can map known formats to extensions - format_to_extension = { - 'jpeg': 'jpg', # 'jpeg' is returned by imghdr for JPEG files - 'png': 'png', - 'heic': 'heic', - # Add more formats as needed - } - - extension = format_to_extension.get(image_format) - return extension - - except Exception as e: - # Handle decoding or identification errors here - print(f"Error: {str(e)}") - - # Return None if the format cannot be determined - return None \ No newline at end of file +def get_file_extension_from_base64(base64_data): + parts = base64_data.split(',') + if len(parts) != 2: + return None # Invalid data URI format + + metadata = parts[0] + + # Extract content type from metadata. + content_type = metadata.split(';')[0][5:] # Remove "data:" prefix + + # Map content type to file extension. + extension_map = { + "image/jpeg": "jpg", + "image/png": "png", + "application/pdf": "pdf", + # Add more content type to extension mappings as needed. + } + + # Use the mapping to get the extension (default to 'dat' if not found). + extension = extension_map.get(content_type, 'dat') + + return extension \ No newline at end of file From 1633075f51ec13738b7e13b752b14295022aecde Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Thu, 14 Sep 2023 22:37:36 -0400 Subject: [PATCH 19/37] feat: implement get applicant query --- app.py | 17 +++++++++++------ chalicelib/db.py | 23 ++++++++++++++++++++++- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index b42e6c6..4a544d8 100644 --- a/app.py +++ b/app.py @@ -69,10 +69,15 @@ def get_all_listings(): @app.route("/listings/{id}", methods=["GET"], cors=True) def get_listing(id): - """Gets a listing from id - """ - data = db.get_item(table_name="zap-listings", key={ - "listingId": id - }) + """Gets a listing from id""" + data = db.get_items(table_name="zap-listings", key={"listingId": id}) - return data \ No newline at end of file + return data + + +@app.route("/applicants/{listing_id}", methods=["GET"], cors=True) +def get_applicants(listing_id): + """Gets all applicants from """ + data = db.get_applicants(table_name="zap-applications", listing_id=listing_id) + + return data diff --git a/chalicelib/db.py b/chalicelib/db.py index 725443d..c75bd15 100644 --- a/chalicelib/db.py +++ b/chalicelib/db.py @@ -1,6 +1,7 @@ import os import boto3 from botocore import errorfactory +from boto3.dynamodb.conditions import Key class DBResource: @@ -36,9 +37,29 @@ def get_all(self, table_name: str): return items @add_env_suffix - def get_item(self, table_name: str, key: dict): + def get_items(self, table_name: str, key: dict): """Gets an item from table_name through key specifier""" table = self.resource.Table(table_name) print(table) item = table.get_item(Key=key) return item + + @add_env_suffix + def get_applicants(self, table_name: str, listing_id: str): + secondary_key_name = "listingId" + secondary_key_value = listing_id + + # Get a reference to the DynamoDB table + table = self.resource.Table(table_name) + + # Use the query method to filter items by the secondary key + # Ensure that global secondary key is set + response = table.query( + IndexName=f"{secondary_key_name}-index", + KeyConditionExpression=Key(secondary_key_name).eq(secondary_key_value) + ) + + if response["ResponseMetadata"]["HTTPStatusCode"] == 200: + return response["Items"] + + return [] \ No newline at end of file From 84bca4c1c9d7f5ce43bacbba51d8b4317cb6df8b Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Fri, 15 Sep 2023 00:24:28 -0400 Subject: [PATCH 20/37] feat: implement get applicant API + refactor --- app.py | 12 +++++++++--- chalicelib/db.py | 10 ++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index 4a544d8..7a304d2 100644 --- a/app.py +++ b/app.py @@ -70,14 +70,20 @@ def get_all_listings(): @app.route("/listings/{id}", methods=["GET"], cors=True) def get_listing(id): """Gets a listing from id""" - data = db.get_items(table_name="zap-listings", key={"listingId": id}) + data = db.get_item(table_name="zap-listings", key={"listingId": id}) return data +@app.route("/applicant/{applicant_id}", methods=["GET"], cors=True) +def get_applicant(applicant_id): + """Get an applicant from """ + data = db.get_item(table_name="zap-applications", key={"applicantId": applicant_id}) + return data + + @app.route("/applicants/{listing_id}", methods=["GET"], cors=True) -def get_applicants(listing_id): +def get_all_applicants(listing_id): """Gets all applicants from """ data = db.get_applicants(table_name="zap-applications", listing_id=listing_id) - return data diff --git a/chalicelib/db.py b/chalicelib/db.py index c75bd15..1c26d50 100644 --- a/chalicelib/db.py +++ b/chalicelib/db.py @@ -37,12 +37,14 @@ def get_all(self, table_name: str): return items @add_env_suffix - def get_items(self, table_name: str, key: dict): + def get_item(self, table_name: str, key: dict) -> dict: """Gets an item from table_name through key specifier""" table = self.resource.Table(table_name) - print(table) - item = table.get_item(Key=key) - return item + response = table.get_item(Key=key) + if response["ResponseMetadata"]["HTTPStatusCode"] == 200: + return response["Item"] + + return {} @add_env_suffix def get_applicants(self, table_name: str, listing_id: str): From 03906b311dce3d9e5f62f63abe891dce7befc703 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sat, 16 Sep 2023 12:12:40 -0400 Subject: [PATCH 21/37] chore: fix readme --- README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/README.md b/README.md index 4f1b66a..2ce971c 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,6 @@ Ensure that you have [AWS CLI](https://aws.amazon.com/cli/) installed. Then, set aws configure ``` -Ensure that you have Node.js [downloaded](https://nodejs.org/en/download) and your node version `v20.x.x`. - -To check your node version: -```bash -node -v -``` - -To update Node.js: -```bash -npm install -g n -``` ## Local Development From f9cc934882892b1eeec7ff4e21e0deaca86fb474 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sat, 16 Sep 2023 14:07:03 -0400 Subject: [PATCH 22/37] refactor: fix auto setting of content type of s3 object --- chalicelib/s3.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/chalicelib/s3.py b/chalicelib/s3.py index ed48149..340333b 100644 --- a/chalicelib/s3.py +++ b/chalicelib/s3.py @@ -5,6 +5,7 @@ from chalicelib.utils import decode_base64 + class S3Client: def __init__(self): self.bucket_name = "whyphi-zap" @@ -18,19 +19,24 @@ def upload_binary_data(self, path: str, data) -> str: path = f"prod/{path}" else: path = f"dev/{path}" - - metadata = { - "Content-Type": "application/pdf", - } - base64_data = data.split(',')[1] + # Split parts of base64 data + parts = base64_data.split(',') + metadata, base64_data = parts[0], parts[1] + + # Extract content type from metadata + content_type = metadata.split(';')[0][5:] # Remove "data:" prefix binary_data = decode_base64(base64_data) + # Upload binary data as object with content type set self.s3.put_object( - Bucket=self.bucket_name, Key=path, Body=binary_data, Metadata=metadata + Bucket=self.bucket_name, Key=path, Body=binary_data, ContentType=content_type ) + # Retrieve endpoint of object s3_endpoint = f"https://{self.bucket_name}.s3.amazonaws.com/" - resume_url = s3_endpoint + path + object_url = s3_endpoint + path - return resume_url + return object_url + + \ No newline at end of file From 7776beb0c883931e1ce6e4520536e4286280b88a Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sat, 16 Sep 2023 14:09:32 -0400 Subject: [PATCH 23/37] chore: fix var assignment bug --- chalicelib/s3.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chalicelib/s3.py b/chalicelib/s3.py index 340333b..05a6ab8 100644 --- a/chalicelib/s3.py +++ b/chalicelib/s3.py @@ -12,7 +12,7 @@ def __init__(self): self.is_prod = os.environ.get("ENV") == "prod" self.s3 = boto3.client("s3") - def upload_binary_data(self, path: str, data) -> str: + def upload_binary_data(self, path: str, data: str) -> str: """Uploads resume to S3 Bucket and returns path""" # Set path if self.is_prod: @@ -21,7 +21,7 @@ def upload_binary_data(self, path: str, data) -> str: path = f"dev/{path}" # Split parts of base64 data - parts = base64_data.split(',') + parts = data.split(',') metadata, base64_data = parts[0], parts[1] # Extract content type from metadata From efb70582bc2e9cafbd2d4597cc7752be28dc1289 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sat, 16 Sep 2023 14:27:48 -0400 Subject: [PATCH 24/37] refactor: implement listing dir on s3 --- app.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app.py b/app.py index 7a304d2..74ece91 100644 --- a/app.py +++ b/app.py @@ -23,14 +23,12 @@ def submit_form(): data["applicantId"] = applicant_id # Upload resume and retrieve, then set link to data - resume_path = f"resume/{data['lastName']}_{data['firstName']}_{applicant_id}.pdf" + resume_path = f"resume/{data['listingId']}/{data['lastName']}_{data['firstName']}_{applicant_id}.pdf" resume_url = s3.upload_binary_data(resume_path, data["resume"]) # Upload photo and retrieve, then set link to data image_extension = get_file_extension_from_base64(data["image"]) - image_path = ( - f"image/{data['lastName']}_{data['firstName']}_{applicant_id}.{image_extension}" - ) + image_path = f"image/{data['listingId']}/{data['lastName']}_{data['firstName']}_{applicant_id}.{image_extension}" image_url = s3.upload_binary_data(image_path, data["image"]) # Reset data properties as S3 url From a4eeb6b591b6fa04bdddb7243310bb3ed57bfd92 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sat, 16 Sep 2023 15:07:14 -0400 Subject: [PATCH 25/37] feat: add IAM role policy for lambda functions --- .chalice/policy-dev.json | 29 +++++++++++++++++++++++++++++ .chalice/policy-prod.json | 29 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .chalice/policy-dev.json create mode 100644 .chalice/policy-prod.json diff --git a/.chalice/policy-dev.json b/.chalice/policy-dev.json new file mode 100644 index 0000000..cc44588 --- /dev/null +++ b/.chalice/policy-dev.json @@ -0,0 +1,29 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "GeneralPolicy", + "Effect": "Allow", + "Action": [ + "dynamodb:BatchGetItem", + "s3:PutObject", + "dynamodb:PutItem", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:Query", + "logs:CreateLogStream", + "logs:CreateLogGroup", + "logs:PutLogEvents" + ], + "Resource": [ + "arn:aws:dynamodb:us-east-1::table/zap-applications-dev", + "arn:aws:dynamodb:us-east-1::table/zap-listings-dev", + "arn:aws:dynamodb:us-east-1:280776660572:table/zap-applications-dev", + "arn:aws:dynamodb:us-east-1:280776660572:table/zap-listings-dev", + "arn:aws:dynamodb:us-east-1:280776660572:table/zap-applications-dev/index/listingId-index", + "arn:*:logs:*:*:*", + "arn:aws:s3:::whyphi-zap/dev/*" + ] + } + ] +} \ No newline at end of file diff --git a/.chalice/policy-prod.json b/.chalice/policy-prod.json new file mode 100644 index 0000000..cc44588 --- /dev/null +++ b/.chalice/policy-prod.json @@ -0,0 +1,29 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "GeneralPolicy", + "Effect": "Allow", + "Action": [ + "dynamodb:BatchGetItem", + "s3:PutObject", + "dynamodb:PutItem", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:Query", + "logs:CreateLogStream", + "logs:CreateLogGroup", + "logs:PutLogEvents" + ], + "Resource": [ + "arn:aws:dynamodb:us-east-1::table/zap-applications-dev", + "arn:aws:dynamodb:us-east-1::table/zap-listings-dev", + "arn:aws:dynamodb:us-east-1:280776660572:table/zap-applications-dev", + "arn:aws:dynamodb:us-east-1:280776660572:table/zap-listings-dev", + "arn:aws:dynamodb:us-east-1:280776660572:table/zap-applications-dev/index/listingId-index", + "arn:*:logs:*:*:*", + "arn:aws:s3:::whyphi-zap/dev/*" + ] + } + ] +} \ No newline at end of file From 903aceea3da1b230d0bf5a20b71a6c9aabb4b298 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sat, 16 Sep 2023 15:09:55 -0400 Subject: [PATCH 26/37] refactor: remove policy autogeneration --- .chalice/config.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.chalice/config.json b/.chalice/config.json index 9885423..65cb5e6 100644 --- a/.chalice/config.json +++ b/.chalice/config.json @@ -3,12 +3,16 @@ "app_name": "zap", "stages": { "dev": { + "autogen_policy": false, + "iam_policy_file": "policy-dev.json", "environment_variables": { "ENV": "dev" }, "api_gateway_stage": "api" }, "prod": { + "autogen_policy": false, + "iam_policy_file": "policy-prod.json", "environment_variables": { "ENV": "prod" }, From c266c50caf32937aec068fd3e9a455e51b346253 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Mon, 9 Oct 2023 11:39:56 -0400 Subject: [PATCH 27/37] chore: modify prod policy --- .chalice/policy-prod.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.chalice/policy-prod.json b/.chalice/policy-prod.json index cc44588..4fd8153 100644 --- a/.chalice/policy-prod.json +++ b/.chalice/policy-prod.json @@ -16,13 +16,13 @@ "logs:PutLogEvents" ], "Resource": [ - "arn:aws:dynamodb:us-east-1::table/zap-applications-dev", - "arn:aws:dynamodb:us-east-1::table/zap-listings-dev", - "arn:aws:dynamodb:us-east-1:280776660572:table/zap-applications-dev", - "arn:aws:dynamodb:us-east-1:280776660572:table/zap-listings-dev", - "arn:aws:dynamodb:us-east-1:280776660572:table/zap-applications-dev/index/listingId-index", + "arn:aws:dynamodb:us-east-1::table/zap-applications-prod", + "arn:aws:dynamodb:us-east-1::table/zap-listings-prod", + "arn:aws:dynamodb:us-east-1:280776660572:table/zap-applications-prod", + "arn:aws:dynamodb:us-east-1:280776660572:table/zap-listings-prod", + "arn:aws:dynamodb:us-east-1:280776660572:table/zap-applications-prod/index/listingId-index", "arn:*:logs:*:*:*", - "arn:aws:s3:::whyphi-zap/dev/*" + "arn:aws:s3:::whyphi-zap/prod/*" ] } ] From 86b76fe316fa8810fa996314f27d646fff64fea5 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sat, 14 Oct 2023 21:25:17 -0400 Subject: [PATCH 28/37] chore: test --- app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app.py b/app.py index 74ece91..e5571cd 100644 --- a/app.py +++ b/app.py @@ -14,6 +14,9 @@ def index(): return {"hello": "world"} +@app.route("/test") +def index(): + return {"test": "test"} @app.route("/submit", methods=["POST"], cors=True) def submit_form(): From b706b2072cb56a9fdd95a93453a0f9d9064006ec Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sat, 14 Oct 2023 21:28:51 -0400 Subject: [PATCH 29/37] chore: set iam role arn --- .chalice/config.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.chalice/config.json b/.chalice/config.json index 9885423..c99d162 100644 --- a/.chalice/config.json +++ b/.chalice/config.json @@ -3,6 +3,8 @@ "app_name": "zap", "stages": { "dev": { + "manage_iam_role": false, + "iam_role_arn": "arn:aws:iam::280776660572:role/zap-dev", "environment_variables": { "ENV": "dev" }, From 0cd682db30f084c92738a5fe9b1d20cbd54e7539 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sat, 14 Oct 2023 21:42:25 -0400 Subject: [PATCH 30/37] fix: set policy auto generate' gsh --- .chalice/config.json | 4 ++-- .chalice/deployed/dev.json | 6 +++--- app.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.chalice/config.json b/.chalice/config.json index c99d162..a742c4e 100644 --- a/.chalice/config.json +++ b/.chalice/config.json @@ -3,8 +3,8 @@ "app_name": "zap", "stages": { "dev": { - "manage_iam_role": false, - "iam_role_arn": "arn:aws:iam::280776660572:role/zap-dev", + "autogen_policy": false, + "iam_policy_file": "policy-dev.json", "environment_variables": { "ENV": "dev" }, diff --git a/.chalice/deployed/dev.json b/.chalice/deployed/dev.json index 2afa883..4b98e45 100644 --- a/.chalice/deployed/dev.json +++ b/.chalice/deployed/dev.json @@ -1,10 +1,10 @@ { "resources": [ { - "name": "default-role", + "name": "api_handler_role", "resource_type": "iam_role", - "role_arn": "arn:aws:iam::280776660572:role/zap-dev", - "role_name": "zap-dev" + "role_arn": "arn:aws:iam::280776660572:role/zap-dev-api_handler", + "role_name": "zap-dev-api_handler" }, { "name": "api_handler", diff --git a/app.py b/app.py index e5571cd..6d7a959 100644 --- a/app.py +++ b/app.py @@ -15,7 +15,7 @@ def index(): return {"hello": "world"} @app.route("/test") -def index(): +def test(): return {"test": "test"} @app.route("/submit", methods=["POST"], cors=True) From 78d4b01b5af5efddbd0942dccb1ec27d681c9d38 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sat, 14 Oct 2023 21:49:34 -0400 Subject: [PATCH 31/37] fix: set prod config pipeline' --- .chalice/config.json | 2 ++ .chalice/deployed/prod.json | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.chalice/config.json b/.chalice/config.json index a742c4e..65cb5e6 100644 --- a/.chalice/config.json +++ b/.chalice/config.json @@ -11,6 +11,8 @@ "api_gateway_stage": "api" }, "prod": { + "autogen_policy": false, + "iam_policy_file": "policy-prod.json", "environment_variables": { "ENV": "prod" }, diff --git a/.chalice/deployed/prod.json b/.chalice/deployed/prod.json index 94e18d1..c97bbb3 100644 --- a/.chalice/deployed/prod.json +++ b/.chalice/deployed/prod.json @@ -1,10 +1,10 @@ { "resources": [ { - "name": "default-role", + "name": "api_handler_role", "resource_type": "iam_role", - "role_arn": "arn:aws:iam::280776660572:role/zap-prod", - "role_name": "zap-prod" + "role_arn": "arn:aws:iam::280776660572:role/zap-prod-api_handler", + "role_name": "zap-prod-api_handler" }, { "name": "api_handler", From 5d5cb86d6b01f5abdd547fd28add0738106b7aa8 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sun, 12 Nov 2023 13:07:57 -0500 Subject: [PATCH 32/37] feat: added visibility toggle API --- app.py | 13 +++++++++++++ chalicelib/db.py | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/app.py b/app.py index 6d7a959..2ae1074 100644 --- a/app.py +++ b/app.py @@ -14,10 +14,12 @@ def index(): return {"hello": "world"} + @app.route("/test") def test(): return {"test": "test"} + @app.route("/submit", methods=["POST"], cors=True) def submit_form(): # Get data as JSON and attach unique id for applicantId @@ -55,6 +57,7 @@ def create_listing(): data = app.current_request.json_body listing_id = str(uuid.uuid4()) data["listingId"] = listing_id + data["isVisible"] = True db.put_data(table_name="zap-listings", data=data) @@ -88,3 +91,13 @@ def get_all_applicants(listing_id): """Gets all applicants from """ data = db.get_applicants(table_name="zap-applications", listing_id=listing_id) return data + + +@app.route("/listings/{id}/toggle/visibility", methods=["PATCH"], cors=True) +def toggle_visibility(id): + """Gets all applicants from """ + data = db.toggle_visibility(table_name="zap-listings", key={"listingId": id}) + if data: + return {"status": True} + + return {"status": False} diff --git a/chalicelib/db.py b/chalicelib/db.py index 1c26d50..681caaf 100644 --- a/chalicelib/db.py +++ b/chalicelib/db.py @@ -42,8 +42,8 @@ def get_item(self, table_name: str, key: dict) -> dict: table = self.resource.Table(table_name) response = table.get_item(Key=key) if response["ResponseMetadata"]["HTTPStatusCode"] == 200: - return response["Item"] - + return response["Item"] + return {} @add_env_suffix @@ -58,10 +58,36 @@ def get_applicants(self, table_name: str, listing_id: str): # Ensure that global secondary key is set response = table.query( IndexName=f"{secondary_key_name}-index", - KeyConditionExpression=Key(secondary_key_name).eq(secondary_key_value) + KeyConditionExpression=Key(secondary_key_name).eq(secondary_key_value), ) if response["ResponseMetadata"]["HTTPStatusCode"] == 200: return response["Items"] - return [] \ No newline at end of file + return [] + + @add_env_suffix + def toggle_visibility(self, table_name: str, key: dict): + """Toggles the visibility boolean for an item identified by the key.""" + # Get a reference to the DynamoDB table + table = self.resource.Table(table_name) + + # Fetch the current item + current_item = table.get_item(Key=key) + + # If the item exists, update the visibility field to the opposite value + if current_item: + current_visibility = current_item["Item"]["isVisible"] + updated_visibility = not current_visibility + + # Update the item with the new visibility value + table.update_item( + Key=key, + UpdateExpression="SET isVisible = :value", + ExpressionAttributeValues={":value": updated_visibility}, + ) + + return True + + # Return None if the item doesn't exist + return None From 9dcc810044aeac70b784c419984f1d269e88e24b Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sun, 12 Nov 2023 13:10:28 -0500 Subject: [PATCH 33/37] chore: added case if visibility doesn't exist --- chalicelib/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chalicelib/db.py b/chalicelib/db.py index 681caaf..f9da0c4 100644 --- a/chalicelib/db.py +++ b/chalicelib/db.py @@ -78,7 +78,7 @@ def toggle_visibility(self, table_name: str, key: dict): # If the item exists, update the visibility field to the opposite value if current_item: current_visibility = current_item["Item"]["isVisible"] - updated_visibility = not current_visibility + updated_visibility = not current_visibility if current_visibility is not None else True # Update the item with the new visibility value table.update_item( From d439c84e6c67c72cf3d229a8be35013362e9867a Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sun, 12 Nov 2023 13:22:38 -0500 Subject: [PATCH 34/37] feat: implement listing model using pydantic --- Pipfile | 1 + Pipfile.lock | 181 +++++++++++++++++++++++++++++----- chalicelib/db.py | 12 ++- chalicelib/models/__init__.py | 0 chalicelib/models/listing.py | 28 ++++++ 5 files changed, 191 insertions(+), 31 deletions(-) create mode 100644 chalicelib/models/__init__.py create mode 100644 chalicelib/models/listing.py diff --git a/Pipfile b/Pipfile index a896cec..983ceb7 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,7 @@ name = "pypi" [packages] chalice = "*" boto3 = "*" +pydantic = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 16031de..cb0d267 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c86eb6d8aefdbf3562d91e701f45ef740a2bc86ba6a28aa6b027ef163c8ce038" + "sha256": "6c1aa9a4dd2c1caa8c03f6a1b443e0323cb80867ebb73394049242276aa36be2" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,14 @@ ] }, "default": { + "annotated-types": { + "hashes": [ + "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", + "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" + ], + "markers": "python_version >= '3.8'", + "version": "==0.6.0" + }, "blessed": { "hashes": [ "sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058", @@ -26,20 +34,20 @@ }, "boto3": { "hashes": [ - "sha256:27560da44099e7e2ee961d3971d8ea659de2e0dc24e78043d1c3027d89b2d8a2", - "sha256:be69cd28e3732b63ad61f6d2429b1eac92428588911a5a7367faa4e129a4738d" + "sha256:98b01bbea27740720a06f7c7bc0132ae4ce902e640aab090cfb99ad3278449c3", + "sha256:adfb915958d7b54d876891ea1599dd83189e35a2442eb41ca52b04ea716180b6" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.28.47" + "version": "==1.28.84" }, "botocore": { "hashes": [ - "sha256:6a60f9601270458102529b17fdcba5551b918f9eedc32bbc2f467e63edfb2662", - "sha256:a0ba5629eb17a37bf449bccda9df6ae652d5755f73145519d5eb244f6963b31b" + "sha256:8913bedb96ad0427660dee083aeaa675466eb662bbf1a47781956b5882aadcc5", + "sha256:d65bc05793d1a8a8c191a739f742876b4b403c5c713dc76beef262d18f7984a2" ], "markers": "python_version >= '3.7'", - "version": "==1.31.47" + "version": "==1.31.84" }, "chalice": { "hashes": [ @@ -81,12 +89,133 @@ "markers": "python_version >= '3.7'", "version": "==23.1.2" }, + "pydantic": { + "hashes": [ + "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7", + "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.4.2" + }, + "pydantic-core": { + "hashes": [ + "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e", + "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33", + "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7", + "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7", + "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea", + "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4", + "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0", + "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7", + "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94", + "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff", + "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82", + "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd", + "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893", + "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e", + "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d", + "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901", + "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9", + "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c", + "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7", + "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891", + "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f", + "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a", + "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9", + "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5", + "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e", + "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a", + "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c", + "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f", + "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514", + "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b", + "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302", + "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096", + "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0", + "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27", + "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884", + "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a", + "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357", + "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430", + "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221", + "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325", + "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4", + "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05", + "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55", + "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875", + "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970", + "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc", + "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6", + "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f", + "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b", + "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d", + "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15", + "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118", + "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee", + "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e", + "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6", + "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208", + "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede", + "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3", + "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e", + "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada", + "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175", + "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a", + "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c", + "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f", + "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58", + "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f", + "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a", + "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a", + "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921", + "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e", + "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904", + "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776", + "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52", + "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf", + "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8", + "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f", + "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b", + "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63", + "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c", + "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f", + "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468", + "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e", + "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab", + "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2", + "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb", + "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb", + "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132", + "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b", + "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607", + "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934", + "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698", + "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e", + "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561", + "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de", + "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b", + "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a", + "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595", + "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402", + "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881", + "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429", + "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5", + "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7", + "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c", + "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531", + "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6", + "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521" + ], + "markers": "python_version >= '3.7'", + "version": "==2.10.1" + }, "python-dateutil": { "hashes": [ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, "python-editor": { @@ -165,11 +294,11 @@ }, "s3transfer": { "hashes": [ - "sha256:b014be3a8a2aab98cfe1abc7229cc5a9a0cf05eb9c1f2b86b230fd8df3f78084", - "sha256:cab66d3380cca3e70939ef2255d01cd8aece6a4907a9528740f668c4b0611861" + "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a", + "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e" ], "markers": "python_version >= '3.7'", - "version": "==0.6.2" + "version": "==0.7.0" }, "setuptools": { "hashes": [ @@ -184,39 +313,39 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "typing-extensions": { "hashes": [ - "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", - "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2" + "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", + "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" ], - "markers": "python_version >= '3.7'", - "version": "==4.7.1" + "markers": "python_version >= '3.8'", + "version": "==4.8.0" }, "urllib3": { "hashes": [ - "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f", - "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14" + "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", + "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.16" + "markers": "python_version < '3.10'", + "version": "==1.26.18" }, "wcwidth": { "hashes": [ - "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e", - "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0" + "sha256:9a929bd8380f6cd9571a968a9c8f4353ca58d7cd812a4822bba831f8d685b223", + "sha256:a675d1a4a2d24ef67096a04b85b02deeecd8e226f57b5e3a72dbb9ed99d27da8" ], - "version": "==0.2.6" + "version": "==0.2.9" }, "wheel": { "hashes": [ - "sha256:0c5ac5ff2afb79ac23ab82bab027a0be7b5dbcf2e54dc50efe4bf507de1f7985", - "sha256:75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8" + "sha256:488609bc63a29322326e05560731bf7bfea8e48ad646e1f5e40d366607de0942", + "sha256:4d4987ce51a49370ea65c0bfd2234e8ce80a12780820d9dc462597a6e60d0841" ], "markers": "python_version >= '3.7'", - "version": "==0.41.2" + "version": "==0.41.3" } }, "develop": {} diff --git a/chalicelib/db.py b/chalicelib/db.py index f9da0c4..514f9d7 100644 --- a/chalicelib/db.py +++ b/chalicelib/db.py @@ -1,7 +1,7 @@ import os import boto3 from botocore import errorfactory -from boto3.dynamodb.conditions import Key +from chalicelib.models.listing import Listing class DBResource: @@ -73,12 +73,14 @@ def toggle_visibility(self, table_name: str, key: dict): table = self.resource.Table(table_name) # Fetch the current item - current_item = table.get_item(Key=key) + curr_listing = Listing.from_dynamodb_item(table.get_item(Key=key)["Item"]) # If the item exists, update the visibility field to the opposite value - if current_item: - current_visibility = current_item["Item"]["isVisible"] - updated_visibility = not current_visibility if current_visibility is not None else True + if curr_listing: + current_visibility = curr_listing.isVisible + updated_visibility = ( + not current_visibility if current_visibility is not None else True + ) # Update the item with the new visibility value table.update_item( diff --git a/chalicelib/models/__init__.py b/chalicelib/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chalicelib/models/listing.py b/chalicelib/models/listing.py new file mode 100644 index 0000000..fe2618f --- /dev/null +++ b/chalicelib/models/listing.py @@ -0,0 +1,28 @@ +from typing import Union, List +from pydantic import BaseModel + + +class Question(BaseModel): + question: str + additional: str + + +class Listing(BaseModel): + listingId: str + dateCreated: str + deadline: str + isVisible: Union[bool, None] + questions: List[Question] + title: str + + @classmethod + def from_dynamodb_item(cls, item: dict): + """Create a Listing instance from a DynamoDB item.""" + return cls( + listingId=item.get("listingId"), + dateCreated=item.get("dateCreated"), + deadline=item.get("deadline"), + isVisible=item.get("isVisible"), + questions=[Question(**q) for q in item.get("questions", [])], + title=item.get("title"), + ) From 4b63914941732bf23c949506e85b3547eb18cc36 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sun, 12 Nov 2023 13:23:28 -0500 Subject: [PATCH 35/37] chore: re-add dynamodb import --- chalicelib/db.py | 1 + 1 file changed, 1 insertion(+) diff --git a/chalicelib/db.py b/chalicelib/db.py index 514f9d7..c7a773f 100644 --- a/chalicelib/db.py +++ b/chalicelib/db.py @@ -2,6 +2,7 @@ import boto3 from botocore import errorfactory from chalicelib.models.listing import Listing +from boto3.dynamodb.conditions import Key class DBResource: From ab908343efbc4b485114fd7ce7ca8d805dabc4bf Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sun, 12 Nov 2023 14:22:01 -0500 Subject: [PATCH 36/37] docs: change docstring of visibility API --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 2ae1074..92416a8 100644 --- a/app.py +++ b/app.py @@ -95,7 +95,7 @@ def get_all_applicants(listing_id): @app.route("/listings/{id}/toggle/visibility", methods=["PATCH"], cors=True) def toggle_visibility(id): - """Gets all applicants from """ + """Toggles visibilility of a given """ data = db.toggle_visibility(table_name="zap-listings", key={"listingId": id}) if data: return {"status": True} From d06cf0b976fd407e46353224159b5eec78097773 Mon Sep 17 00:00:00 2001 From: Jin Young Bang Date: Sat, 18 Nov 2023 15:59:13 -0500 Subject: [PATCH 37/37] refactor: add messages for errors in API --- app.py | 19 ++++++++++++++----- chalicelib/db.py | 6 +++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app.py b/app.py index 92416a8..2f38787 100644 --- a/app.py +++ b/app.py @@ -3,6 +3,7 @@ from chalicelib.s3 import S3Client from chalicelib.utils import get_file_extension_from_base64 + import uuid app = Chalice(app_name="zap") @@ -96,8 +97,16 @@ def get_all_applicants(listing_id): @app.route("/listings/{id}/toggle/visibility", methods=["PATCH"], cors=True) def toggle_visibility(id): """Toggles visibilility of a given """ - data = db.toggle_visibility(table_name="zap-listings", key={"listingId": id}) - if data: - return {"status": True} - - return {"status": False} + try: + # Perform visibility toggle in the database + data = db.toggle_visibility(table_name="zap-listings", key={"listingId": id}) + + # Check the result and return the appropriate response + if data: + return {"status": True} + else: + return {"status": False, "message": "Invalid listing ID"}, 400 + + except Exception as e: + app.log.error(f"An error occurred: {str(e)}") + return {"status": False, "message": "Internal Server Error"}, 500 \ No newline at end of file diff --git a/chalicelib/db.py b/chalicelib/db.py index c7a773f..f8dca83 100644 --- a/chalicelib/db.py +++ b/chalicelib/db.py @@ -74,7 +74,11 @@ def toggle_visibility(self, table_name: str, key: dict): table = self.resource.Table(table_name) # Fetch the current item - curr_listing = Listing.from_dynamodb_item(table.get_item(Key=key)["Item"]) + listing_item = table.get_item(Key=key) + if "Item" not in listing_item: + return None + + curr_listing = Listing.from_dynamodb_item(listing_item["Item"]) # If the item exists, update the visibility field to the opposite value if curr_listing: