diff --git a/.chalice/config.json b/.chalice/config.json index 5583a3c..65cb5e6 100644 --- a/.chalice/config.json +++ b/.chalice/config.json @@ -1,10 +1,22 @@ { - "version": "2.0", - "app_name": "zap", - "stages": { - "dev": { - "api_gateway_stage": "api" - } + "version": "2.0", + "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" + }, + "api_gateway_stage": "api" } } - \ No newline at end of file +} diff --git a/.chalice/deployed/dev.json b/.chalice/deployed/dev.json new file mode 100644 index 0000000..4b98e45 --- /dev/null +++ b/.chalice/deployed/dev.json @@ -0,0 +1,23 @@ +{ + "resources": [ + { + "name": "api_handler_role", + "resource_type": "iam_role", + "role_arn": "arn:aws:iam::280776660572:role/zap-dev-api_handler", + "role_name": "zap-dev-api_handler" + }, + { + "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" +} diff --git a/.chalice/deployed/prod.json b/.chalice/deployed/prod.json new file mode 100644 index 0000000..c97bbb3 --- /dev/null +++ b/.chalice/deployed/prod.json @@ -0,0 +1,23 @@ +{ + "resources": [ + { + "name": "api_handler_role", + "resource_type": "iam_role", + "role_arn": "arn:aws:iam::280776660572:role/zap-prod-api_handler", + "role_name": "zap-prod-api_handler" + }, + { + "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" +} 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..4fd8153 --- /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-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/prod/*" + ] + } + ] +} \ No newline at end of file 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/Pipfile b/Pipfile index e43344d..983ceb7 100644 --- a/Pipfile +++ b/Pipfile @@ -5,6 +5,8 @@ name = "pypi" [packages] chalice = "*" +boto3 = "*" +pydantic = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 9dd429c..cb0d267 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "efc53bf590c5d7898408f63b08a384d064a1000669e752018a3a217380a8f86a" + "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", @@ -24,13 +32,22 @@ "markers": "python_version >= '2.7'", "version": "==1.20.0" }, + "boto3": { + "hashes": [ + "sha256:98b01bbea27740720a06f7c7bc0132ae4ce902e640aab090cfb99ad3278449c3", + "sha256:adfb915958d7b54d876891ea1599dd83189e35a2442eb41ca52b04ea716180b6" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "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": [ @@ -64,6 +81,135 @@ "markers": "python_version >= '3.7'", "version": "==1.0.1" }, + "pip": { + "hashes": [ + "sha256:0e7c86f486935893c708287b30bd050a36ac827ec7fe5e43fe7cb198dd835fba", + "sha256:3ef6ac33239e4027d9a5598a381b9d30880a1477e50039db2eac6e8a8f6d1b18" + ], + "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", @@ -146,6 +292,22 @@ "markers": "python_version >= '3.7'", "version": "==4.0.5" }, + "s3transfer": { + "hashes": [ + "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a", + "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e" + ], + "markers": "python_version >= '3.7'", + "version": "==0.7.0" + }, + "setuptools": { + "hashes": [ + "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87", + "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a" + ], + "markers": "python_version >= '3.8'", + "version": "==68.2.2" + }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -156,34 +318,34 @@ }, "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/README.md b/README.md new file mode 100644 index 0000000..2ce971c --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# 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. diff --git a/app.py b/app.py index 33d5f68..2f38787 100644 --- a/app.py +++ b/app.py @@ -1,29 +1,112 @@ from chalice import Chalice +from chalicelib.db import DBResource +from chalicelib.s3 import S3Client +from chalicelib.utils import get_file_extension_from_base64 -app = Chalice(app_name='test') +import uuid -@app.route('/') +app = Chalice(app_name="zap") +db = DBResource() +s3 = S3Client() + + +@app.route("/") def index(): - return {'hello': 'world'} - - -# 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. -# + 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 + 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['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['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 + data["resume"], data["image"] = resume_url, image_url + + # Upload data to DynamoDB + db.put_data(table_name="zap-applications", data=data) + + return {"msg": True, "resumeUrl": resume_url} + + +@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"], cors=True) +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 + data["isVisible"] = True + + db.put_data(table_name="zap-listings", data=data) + + return {"msg": True} + + +@app.route("/listings", methods=["GET"], cors=True) +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 + + +@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_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): + """Toggles visibilility of a given """ + 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 new file mode 100644 index 0000000..f8dca83 --- /dev/null +++ b/chalicelib/db.py @@ -0,0 +1,100 @@ +import os +import boto3 +from botocore import errorfactory +from chalicelib.models.listing import Listing +from boto3.dynamodb.conditions import Key + + +class DBResource: + def __init__(self): + self.is_prod = os.environ.get("ENV") == "prod" + self.resource = boto3.resource("dynamodb") + self.primary_keys = {"zap-listings": "listingId"} + + def add_env_suffix(func): + def wrapper(self, table_name: str, *args, **kwargs): + if self.is_prod: + table_name += "-prod" + else: + table_name += "-dev" + + return func(self, table_name, *args, **kwargs) + + 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): + 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 + + @add_env_suffix + 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) + 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): + 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 [] + + @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 + 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: + 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( + Key=key, + UpdateExpression="SET isVisible = :value", + ExpressionAttributeValues={":value": updated_visibility}, + ) + + return True + + # Return None if the item doesn't exist + return None 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 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"), + ) diff --git a/chalicelib/s3.py b/chalicelib/s3.py new file mode 100644 index 0000000..05a6ab8 --- /dev/null +++ b/chalicelib/s3.py @@ -0,0 +1,42 @@ +import boto3 +import os +import json +from datetime import datetime +from chalicelib.utils import decode_base64 + + + +class S3Client: + def __init__(self): + self.bucket_name = "whyphi-zap" + self.is_prod = os.environ.get("ENV") == "prod" + self.s3 = boto3.client("s3") + + def upload_binary_data(self, path: str, data: str) -> str: + """Uploads resume to S3 Bucket and returns path""" + # Set path + if self.is_prod: + path = f"prod/{path}" + else: + path = f"dev/{path}" + + # Split parts of base64 data + parts = 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, ContentType=content_type + ) + + # Retrieve endpoint of object + s3_endpoint = f"https://{self.bucket_name}.s3.amazonaws.com/" + object_url = s3_endpoint + path + + return object_url + + \ No newline at end of file diff --git a/chalicelib/utils.py b/chalicelib/utils.py new file mode 100644 index 0000000..9dfab87 --- /dev/null +++ b/chalicelib/utils.py @@ -0,0 +1,31 @@ +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_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