diff --git a/Pipfile b/Pipfile index 5a3b32f..32c9c52 100644 --- a/Pipfile +++ b/Pipfile @@ -4,12 +4,15 @@ verify_ssl = true name = "pypi" [packages] +boto3 = "*" +boto3-stubs = {extras = ["essential","ses"], version = "*"} click = "*" sentry-sdk = "*" [dev-packages] black = "*" coveralls = "*" +moto="*" mypy = "*" pre-commit = "*" pytest = "*" diff --git a/Pipfile.lock b/Pipfile.lock index e124974..0252f65 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9ff6fc01892c58837f374247f3500dd82df50c99be9905396f7fc69d785a3f1a" + "sha256": "3f7ddd448e9b9b3c1ce75b9627bdd4676e3c5367183f1017af5936052d4d16c8" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,43 @@ ] }, "default": { + "boto3": { + "hashes": [ + "sha256:473438feafe77d29fbea532a91a65de0d8751a4fa5822127218710a205e28e7a", + "sha256:ccb1a365d3084de53b58f8dfc056462f49b16931c139f4c8ac5f0bca8cb8fe81" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.35.73" + }, + "boto3-stubs": { + "extras": [ + "essential", + "ses" + ], + "hashes": [ + "sha256:b935f0b62be1e18445f63cd9f5bbb4fe9a792d99efa9eb7f37b641ed4a6e70e0", + "sha256:d1c072dfa59fbe0d91ba8e8966e844d9eb79ccc5f59e49914f796f29cd96a14d" + ], + "markers": "python_version >= '3.8'", + "version": "==1.35.73" + }, + "botocore": { + "hashes": [ + "sha256:8a6a0f5ad119e38d850571df8c625dbad66aec1b20c15f84cdcb95258f9f1edb", + "sha256:b2e3ecdd1769f011f72c4c0d0094570ba125f4ca327f24269e4d68eb5d9878b9" + ], + "markers": "python_version >= '3.8'", + "version": "==1.35.73" + }, + "botocore-stubs": { + "hashes": [ + "sha256:54f7bcc325382050ae6aa839163f93f5c4e777db9c0fd2da3ad0744720895fbe", + "sha256:e9a20b0a29621674b46225fdb88bf00a0bca5216413d717895b75ba2dd63c6cc" + ], + "markers": "python_version >= '3.8'", + "version": "==1.35.73" + }, "certifi": { "hashes": [ "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", @@ -33,14 +70,118 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, + "jmespath": { + "hashes": [ + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.1" + }, + "mypy-boto3-cloudformation": { + "hashes": [ + "sha256:aba213f3411a65096a8d95633c36e0c57a775ac6ac9ccf1e6fd9bea4002073bc", + "sha256:d1a1500df811ac8ebd459640f5b31c14daac784d8a00fc4f67bc6eb391e7b5a8" + ], + "version": "==1.35.64" + }, + "mypy-boto3-dynamodb": { + "hashes": [ + "sha256:187915c781f352bc79d35b08a094605515ecc54f30107f629972c3358b864a5c", + "sha256:92eac35c49e9f3ff23a4ad6dee5dc54e410e0c49a98b4d93493c7000ebe74568" + ], + "version": "==1.35.60" + }, + "mypy-boto3-ec2": { + "hashes": [ + "sha256:3206cd6da473647cdefa5dcec4121b4a83778f49ee540ca4b8aeb6c337975b69", + "sha256:d2ff43ad1c42655cbcbb06d11dff74b3827503d80a99a78098ab52ba0fbb7235" + ], + "version": "==1.35.72" + }, + "mypy-boto3-lambda": { + "hashes": [ + "sha256:00499898236fe423c9292f77644102d4bd6699b3c16b8c4062eb759c022447f5", + "sha256:577a9465ac63ac564efc2755a7e72c28a9d2f496747c1faf242cb13d5017b262" + ], + "version": "==1.35.68" + }, + "mypy-boto3-rds": { + "hashes": [ + "sha256:4c345e616a7767953284a0d54ab6dbabd8b068fe353b34194b79364b47176b61", + "sha256:acd87fdfd12cc8f7298f586734f5c5e7afb0fcd6da8154a10cccb5730cc5c799" + ], + "version": "==1.35.72" + }, + "mypy-boto3-s3": { + "hashes": [ + "sha256:571e659c1d355499d5e5070f33e613a1e251e6f5d2a57d535c5eaef52ebb6a86", + "sha256:b2a18ca57079659eb602dcfc4abb56425c793ccb1939826e401d4f2ddf9128b0" + ], + "version": "==1.35.72" + }, + "mypy-boto3-ses": { + "hashes": [ + "sha256:5706a6802da50419d5bf077cd0f00ad3d98018d81f8e8cab38ff9871cc65004c", + "sha256:5a3b239ef05ff9c6b7988f33b77f5f63eaf44e1615b3269b0d394cdd665f4ef1" + ], + "version": "==1.35.68" + }, + "mypy-boto3-sqs": { + "hashes": [ + "sha256:61752f1c2bf2efa3815f64d43c25b4a39dbdbd9e472ae48aa18d7c6d2a7a6eb8", + "sha256:9fd6e622ed231c06f7542ba6f8f0eea92046cace24defa95d0d0ce04e7caee0c" + ], + "version": "==1.35.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" + }, + "s3transfer": { + "hashes": [ + "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e", + "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7" + ], + "markers": "python_version >= '3.8'", + "version": "==0.10.4" + }, "sentry-sdk": { "hashes": [ - "sha256:0dc21febd1ab35c648391c664df96f5f79fb0d92d7d4225cd9832e53a617cafd", - "sha256:ee70e27d1bbe4cd52a38e1bd28a5fadb9b17bc29d91b5f2b97ae29c0a7610442" + "sha256:7b0b3b709dee051337244a09a30dbf6e95afe0d34a1f8b430d45e0982a7c125b", + "sha256:ee4a4d2ae8bfe3cac012dcf3e4607975904c137e1738116549fc3dbbb6ff0e36" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2.18.0" + "version": "==2.19.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "types-awscrt": { + "hashes": [ + "sha256:0d362a5d62d68ca4216f458172f41c1123ec04791d68364de8ee8b61b528b262", + "sha256:a20b425dabb258bc3d07a5e7de503fd9558dd1542d72de796e74e402c6d493b2" + ], + "markers": "python_version >= '3.8'", + "version": "==0.23.1" + }, + "types-s3transfer": { + "hashes": [ + "sha256:03123477e3064c81efe712bf9d372c7c72f2790711431f9baa59cf96ea607267", + "sha256:22ac1aabc98f9d7f2928eb3fb4d5c02bf7435687f0913345a97dd3b84d0c217d" + ], + "markers": "python_version >= '3.8'", + "version": "==0.10.4" }, "urllib3": { "hashes": [ @@ -81,6 +222,23 @@ "markers": "python_version >= '3.9'", "version": "==24.10.0" }, + "boto3": { + "hashes": [ + "sha256:473438feafe77d29fbea532a91a65de0d8751a4fa5822127218710a205e28e7a", + "sha256:ccb1a365d3084de53b58f8dfc056462f49b16931c139f4c8ac5f0bca8cb8fe81" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.35.73" + }, + "botocore": { + "hashes": [ + "sha256:8a6a0f5ad119e38d850571df8c625dbad66aec1b20c15f84cdcb95258f9f1edb", + "sha256:b2e3ecdd1769f011f72c4c0d0094570ba125f4ca327f24269e4d68eb5d9878b9" + ], + "markers": "python_version >= '3.8'", + "version": "==1.35.73" + }, "certifi": { "hashes": [ "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", @@ -89,6 +247,79 @@ "markers": "python_version >= '3.6'", "version": "==2024.8.30" }, + "cffi": { + "hashes": [ + "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", + "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", + "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", + "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", + "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", + "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", + "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", + "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", + "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", + "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", + "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", + "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", + "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", + "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", + "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", + "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", + "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", + "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", + "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", + "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", + "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", + "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", + "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", + "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", + "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", + "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", + "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", + "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", + "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", + "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", + "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", + "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", + "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", + "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", + "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", + "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", + "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", + "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", + "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", + "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", + "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", + "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", + "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", + "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", + "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", + "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", + "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", + "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", + "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", + "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", + "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", + "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", + "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", + "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", + "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", + "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", + "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", + "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", + "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", + "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", + "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.17.1" + }, "cfgv": { "hashes": [ "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", @@ -222,71 +453,71 @@ "toml" ], "hashes": [ - "sha256:0266b62cbea568bd5e93a4da364d05de422110cbed5056d69339bd5af5685433", - "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529", - "sha256:0ddcb70b3a3a57581b450571b31cb774f23eb9519c2aaa6176d3a84c9fc57671", - "sha256:108bb458827765d538abcbf8288599fee07d2743357bdd9b9dad456c287e121e", - "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42", - "sha256:1a5407a75ca4abc20d6252efeb238377a71ce7bda849c26c7a9bece8680a5d99", - "sha256:2bc3e45c16564cc72de09e37413262b9f99167803e5e48c6156bccdfb22c8327", - "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8", - "sha256:34d23e28ccb26236718a3a78ba72744212aa383141961dd6825f6595005c8b06", - "sha256:37a15573f988b67f7348916077c6d8ad43adb75e478d0910957394df397d2874", - "sha256:3c0317288f032221d35fa4cbc35d9f4923ff0dfd176c79c9b356e8ef8ef2dff4", - "sha256:3c42ec2c522e3ddd683dec5cdce8e62817afb648caedad9da725001fa530d354", - "sha256:3c6b24007c4bcd0b19fac25763a7cac5035c735ae017e9a349b927cfc88f31c1", - "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab", - "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3", - "sha256:49ed5ee4109258973630c1f9d099c7e72c5c36605029f3a91fe9982c6076c82b", - "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37", - "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd", - "sha256:5e444b8e88339a2a67ce07d41faabb1d60d1004820cee5a2c2b54e2d8e429a0f", - "sha256:60dcf7605c50ea72a14490d0756daffef77a5be15ed1b9fea468b1c7bda1bc3b", - "sha256:623e6965dcf4e28a3debaa6fcf4b99ee06d27218f46d43befe4db1c70841551c", - "sha256:673184b3156cba06154825f25af33baa2671ddae6343f23175764e65a8c4c30b", - "sha256:6cf96ceaa275f071f1bea3067f8fd43bec184a25a962c754024c973af871e1b7", - "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3", - "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808", - "sha256:796c9b107d11d2d69e1849b2dfe41730134b526a49d3acb98ca02f4985eeff7a", - "sha256:7c07de0d2a110f02af30883cd7dddbe704887617d5c27cf373362667445a4c76", - "sha256:7e61b0e77ff4dddebb35a0e8bb5a68bf0f8b872407d8d9f0c726b65dfabe2469", - "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55", - "sha256:850cfd2d6fc26f8346f422920ac204e1d28814e32e3a58c19c91980fa74d8289", - "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc", - "sha256:90746521206c88bdb305a4bf3342b1b7316ab80f804d40c536fc7d329301ee13", - "sha256:951aade8297358f3618a6e0660dc74f6b52233c42089d28525749fc8267dccd2", - "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30", - "sha256:987a8e3da7da4eed10a20491cf790589a8e5e07656b6dc22d3814c4d88faf163", - "sha256:9c2eb378bebb2c8f65befcb5147877fc1c9fbc640fc0aad3add759b5df79d55d", - "sha256:a1ab9763d291a17b527ac6fd11d1a9a9c358280adb320e9c2672a97af346ac2c", - "sha256:a3b925300484a3294d1c70f6b2b810d6526f2929de954e5b6be2bf8caa1f12c1", - "sha256:acbb8af78f8f91b3b51f58f288c0994ba63c646bc1a8a22ad072e4e7e0a49f1c", - "sha256:ad32a981bcdedb8d2ace03b05e4fd8dace8901eec64a532b00b15217d3677dd2", - "sha256:aee9cf6b0134d6f932d219ce253ef0e624f4fa588ee64830fcba193269e4daa3", - "sha256:af05bbba896c4472a29408455fe31b3797b4d8648ed0a2ccac03e074a77e2314", - "sha256:b6cce5c76985f81da3769c52203ee94722cd5d5889731cd70d31fee939b74bf0", - "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384", - "sha256:c132b5a22821f9b143f87446805e13580b67c670a548b96da945a8f6b4f2efbb", - "sha256:c296263093f099da4f51b3dff1eff5d4959b527d4f2f419e16508c5da9e15e8c", - "sha256:c973b2fe4dc445cb865ab369df7521df9c27bf40715c837a113edaa2aa9faf45", - "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a", - "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24", - "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8", - "sha256:df002e59f2d29e889c37abd0b9ee0d0e6e38c24f5f55d71ff0e09e3412a340ec", - "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56", - "sha256:e25bacb53a8c7325e34d45dddd2f2fbae0dbc230d0e2642e264a64e17322a777", - "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b", - "sha256:e5f2a0f161d126ccc7038f1f3029184dbdf8f018230af17ef6fd6a707a5b881f", - "sha256:e69ad502f1a2243f739f5bd60565d14a278be58be4c137d90799f2c263e7049a", - "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d", - "sha256:f07ff574986bc3edb80e2c36391678a271d555f91fd1d332a1e0f4b5ea4b6ea9", - "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413", - "sha256:f3e8796434a8106b3ac025fd15417315d7a58ee3e600ad4dbcfddc3f4b14342c", - "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b", - "sha256:fd49c01e5057a451c30c9b892948976f5d38f2cbd04dc556a82743ba8e27ed8c" + "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5", + "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf", + "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb", + "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638", + "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4", + "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc", + "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed", + "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a", + "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d", + "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649", + "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c", + "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b", + "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4", + "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443", + "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83", + "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee", + "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e", + "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e", + "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3", + "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0", + "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb", + "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076", + "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb", + "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787", + "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1", + "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e", + "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce", + "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801", + "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764", + "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365", + "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf", + "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6", + "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71", + "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002", + "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4", + "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c", + "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8", + "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4", + "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146", + "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", + "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea", + "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4", + "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad", + "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28", + "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451", + "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50", + "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779", + "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63", + "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e", + "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc", + "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022", + "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d", + "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94", + "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b", + "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d", + "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331", + "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a", + "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0", + "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee", + "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92", + "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a", + "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9" ], "markers": "python_version >= '3.9'", - "version": "==7.6.7" + "version": "==7.6.8" }, "coveralls": { "hashes": [ @@ -297,6 +528,41 @@ "markers": "python_version < '3.13' and python_version >= '3.8'", "version": "==4.0.1" }, + "cryptography": { + "hashes": [ + "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", + "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", + "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", + "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", + "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", + "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385", + "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", + "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", + "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", + "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", + "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", + "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", + "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", + "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba", + "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", + "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", + "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", + "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", + "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", + "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", + "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", + "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", + "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", + "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", + "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", + "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", + "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", + "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", + "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4" + ], + "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'", + "version": "==44.0.0" + }, "distlib": { "hashes": [ "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", @@ -320,11 +586,11 @@ }, "identify": { "hashes": [ - "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3", - "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd" + "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02", + "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd" ], "markers": "python_version >= '3.9'", - "version": "==2.6.2" + "version": "==2.6.3" }, "idna": { "hashes": [ @@ -342,6 +608,98 @@ "markers": "python_version >= '3.7'", "version": "==2.0.0" }, + "jinja2": { + "hashes": [ + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.4" + }, + "jmespath": { + "hashes": [ + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.1" + }, + "markupsafe": { + "hashes": [ + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" + ], + "markers": "python_version >= '3.9'", + "version": "==3.0.2" + }, + "moto": { + "hashes": [ + "sha256:daf47b8a1f5f190cd3eaa40018a643f38e542277900cf1db7f252cedbfed998f", + "sha256:defae32e834ba5674f77cbbe996b41dc248dd81289af8032fa3e847284409b29" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==5.0.22" + }, "mypy": { "hashes": [ "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", @@ -438,14 +796,30 @@ "markers": "python_version >= '3.9'", "version": "==4.0.1" }, + "pycparser": { + "hashes": [ + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + ], + "markers": "python_version >= '3.8'", + "version": "==2.22" + }, "pytest": { "hashes": [ - "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", - "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", + "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==8.3.3" + "version": "==8.3.4" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" }, "pyyaml": { "hashes": [ @@ -514,30 +888,54 @@ "markers": "python_version >= '3.8'", "version": "==2.32.3" }, + "responses": { + "hashes": [ + "sha256:521efcbc82081ab8daa588e08f7e8a64ce79b91c39f6e62199b19159bea7dbcb", + "sha256:617b9247abd9ae28313d57a75880422d55ec63c29d33d629697590a034358dba" + ], + "markers": "python_version >= '3.8'", + "version": "==0.25.3" + }, "ruff": { "hashes": [ - "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05", - "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109", - "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc", - "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f", - "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a", - "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172", - "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20", - "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd", - "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6", - "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac", - "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06", - "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea", - "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a", - "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7", - "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478", - "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2", - "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63", - "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452" + "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871", + "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1", + "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d", + "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6", + "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5", + "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f", + "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1", + "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737", + "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790", + "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9", + "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540", + "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26", + "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c", + "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa", + "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087", + "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209", + "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5", + "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.7.4" + "version": "==0.8.1" + }, + "s3transfer": { + "hashes": [ + "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e", + "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7" + ], + "markers": "python_version >= '3.8'", + "version": "==0.10.4" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "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": [ @@ -557,11 +955,27 @@ }, "virtualenv": { "hashes": [ - "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba", - "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4" + "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0", + "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa" ], "markers": "python_version >= '3.8'", - "version": "==20.27.1" + "version": "==20.28.0" + }, + "werkzeug": { + "hashes": [ + "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", + "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" + ], + "markers": "python_version >= '3.9'", + "version": "==3.1.3" + }, + "xmltodict": { + "hashes": [ + "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553", + "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac" + ], + "markers": "python_version >= '3.6'", + "version": "==0.14.2" } } } diff --git a/README.md b/README.md index 52d14a7..2a10c84 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,13 @@ Description of the app ```shell SENTRY_DSN=### If set to a valid Sentry DSN, enables Sentry exception monitoring. This is not needed for local development. WORKSPACE=### Set to `dev` for local development, this will be set to `stage` and `prod` in those environments by Terraform. +AWS_REGION_NAME=### Default AWS region. ``` +### Optional + +```shell +LOG_LEVEL=### Logging level. Defaults to 'INFO'. +``` diff --git a/dsc/cli.py b/dsc/cli.py index 49d75e4..2c79328 100644 --- a/dsc/cli.py +++ b/dsc/cli.py @@ -1,12 +1,14 @@ import logging from datetime import timedelta +from io import StringIO from time import perf_counter import click -from dsc.config import configure_logger, configure_sentry +from dsc.config import Config logger = logging.getLogger(__name__) +CONFIG = Config() @click.command() @@ -15,9 +17,11 @@ ) def main(*, verbose: bool) -> None: start_time = perf_counter() + stream = StringIO() root_logger = logging.getLogger() - logger.info(configure_logger(root_logger, verbose=verbose)) - logger.info(configure_sentry()) + logger.info(CONFIG.configure_logger(root_logger, stream, verbose=verbose)) + logger.info(CONFIG.configure_sentry()) + CONFIG.check_required_env_vars() logger.info("Running process") # Do things here! diff --git a/dsc/config.py b/dsc/config.py index 0e446c5..7e39ee6 100644 --- a/dsc/config.py +++ b/dsc/config.py @@ -1,33 +1,63 @@ import logging import os +from collections.abc import Iterable +from io import StringIO import sentry_sdk -def configure_logger(logger: logging.Logger, *, verbose: bool) -> str: - if verbose: - logging.basicConfig( - format="%(asctime)s %(levelname)s %(name)s.%(funcName)s() line %(lineno)d: " - "%(message)s" - ) - logger.setLevel(logging.DEBUG) - for handler in logging.root.handlers: - handler.addFilter(logging.Filter("dsc")) - else: - logging.basicConfig( - format="%(asctime)s %(levelname)s %(name)s.%(funcName)s(): %(message)s" +class Config: + REQUIRED_ENV_VARS: Iterable[str] = [ + "WORKSPACE", + "SENTRY_DSN", + "AWS_REGION_NAME", + ] + + OPTIONAL_ENV_VARS: Iterable[str] = ["LOG_LEVEL"] + + def __getattr__(self, name: str) -> str | None: + """Provide dot notation access to configurations and env vars on this class.""" + if name in self.REQUIRED_ENV_VARS or name in self.OPTIONAL_ENV_VARS: + return os.getenv(name) + message = f"'{name}' not a valid configuration variable" + raise AttributeError(message) + + def check_required_env_vars(self) -> None: + """Method to raise exception if required env vars not set.""" + missing_vars = [var for var in self.REQUIRED_ENV_VARS if not os.getenv(var)] + if missing_vars: + message = f"Missing required environment variables: {', '.join(missing_vars)}" + raise OSError(message) + + def configure_logger( + self, logger: logging.Logger, stream: StringIO, *, verbose: bool + ) -> str: + logging_format_base = "%(asctime)s %(levelname)s %(name)s.%(funcName)s()" + logger.addHandler(logging.StreamHandler(stream)) + + if verbose: + log_method, log_level = logger.debug, logging.DEBUG + template = logging_format_base + " line %(lineno)d: %(message)s" + for handler in logging.root.handlers: + handler.addFilter(logging.Filter("dsc")) + else: + log_method, log_level = logger.info, logging.INFO + template = logging_format_base + ": %(message)s" + + logger.setLevel(log_level) + logging.basicConfig(format=template) + logger.addHandler(logging.StreamHandler(stream)) + log_method(f"{logging.getLevelName(logger.getEffectiveLevel())}") + + return ( + f"Logger '{logger.name}' configured with level=" + f"{logging.getLevelName(logger.getEffectiveLevel())}" ) - logger.setLevel(logging.INFO) - return ( - f"Logger '{logger.name}' configured with level=" - f"{logging.getLevelName(logger.getEffectiveLevel())}" - ) - - -def configure_sentry() -> str: - env = os.getenv("WORKSPACE") - sentry_dsn = os.getenv("SENTRY_DSN") - if sentry_dsn and sentry_dsn.lower() != "none": - sentry_sdk.init(sentry_dsn, environment=env) - return f"Sentry DSN found, exceptions will be sent to Sentry with env={env}" - return "No Sentry DSN found, exceptions will not be sent to Sentry" + + def configure_sentry(self) -> str: + env = self.WORKSPACE + sentry_dsn = self.SENTRY_DSN + if sentry_dsn and sentry_dsn.lower() != "none": + sentry_sdk.init(sentry_dsn, environment=env) + return f"Sentry DSN found, exceptions will be sent to Sentry with env={env}" + return "No Sentry DSN found, exceptions will not be sent to Sentry" diff --git a/dsc/s3.py b/dsc/s3.py new file mode 100644 index 0000000..e00146d --- /dev/null +++ b/dsc/s3.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +import boto3 + +if TYPE_CHECKING: + from collections.abc import Iterator + + from mypy_boto3_s3.type_defs import PutObjectOutputTypeDef + +logger = logging.getLogger(__name__) + + +class S3Client: + """A class to perform common S3 operations for this application.""" + + def __init__(self) -> None: + self.client = boto3.client("s3") + + def archive_file_with_new_key( + self, bucket: str, key: str, archived_key_prefix: str + ) -> None: + """Update the key of the specified file to archive it from processing. + + Args: + bucket: The S3 bucket containing the files to be archived. + key: The key of the file to archive. + archived_key_prefix: The prefix to be applied to the archived file. + """ + self.client.copy_object( + Bucket=bucket, + CopySource=f"{bucket}/{key}", + Key=f"{archived_key_prefix}/{key}", + ) + self.client.delete_object( + Bucket=bucket, + Key=key, + ) + + def put_file( + self, file_content: str | bytes, bucket: str, key: str + ) -> PutObjectOutputTypeDef: + """Put a file in a specified S3 bucket with a specified key. + + Args: + file_content: The content of the file to be uploaded. + bucket: The S3 bucket where the file will be uploaded. + key: The key to be used for the uploaded file. + """ + response = self.client.put_object( + Body=file_content, + Bucket=bucket, + Key=key, + ) + logger.debug(f"'{key}' uploaded to S3") + return response + + def get_files_iter( + self, bucket: str, file_type: str, excluded_key_prefix: str + ) -> Iterator[str]: + """Retrieve file based on file type, bucket, and without excluded prefix. + + Args: + bucket: The S3 bucket to search. + file_type: The file type to retrieve. + excluded_key_prefix: Files with this key prefix will not be retrieved. + """ + paginator = self.client.get_paginator("list_objects_v2") + page_iterator = paginator.paginate(Bucket=bucket) + + for page in page_iterator: + files = [ + content["Key"] + for content in page["Contents"] + if content["Key"].endswith(file_type) + and excluded_key_prefix not in content["Key"] + ] + yield from files diff --git a/pyproject.toml b/pyproject.toml index 8900c6b..e0345ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,21 +28,20 @@ ignore = [ # default "ANN101", "ANN102", - "COM812", - "D107", - "N812", - "PTH", - - # project-specific "C90", + "COM812", "D100", "D101", "D102", "D103", "D104", + "D107", + "G004", + "N812", "PLR0912", "PLR0913", "PLR0915", + "PTH", "S320", "S321", ] diff --git a/tests/conftest.py b/tests/conftest.py index 42c8c64..00105c2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,44 @@ +from io import StringIO + +import boto3 import pytest from click.testing import CliRunner +from moto import mock_aws + +from dsc.config import Config +from dsc.s3 import S3Client @pytest.fixture(autouse=True) def _test_env(monkeypatch): monkeypatch.setenv("SENTRY_DSN", "None") monkeypatch.setenv("WORKSPACE", "test") + monkeypatch.setenv("AWS_REGION_NAME", "us-east-1") + + +@pytest.fixture +def config_instance() -> Config: + return Config() + + +@pytest.fixture +def mocked_s3(config_instance): + with mock_aws(): + s3 = boto3.client("s3", region_name=config_instance.AWS_REGION_NAME) + s3.create_bucket(Bucket="awd") + yield s3 @pytest.fixture def runner(): return CliRunner() + + +@pytest.fixture +def s3_client(): + return S3Client() + + +@pytest.fixture +def stream(): + return StringIO() diff --git a/tests/test_config.py b/tests/test_config.py index fbeb4cc..8d6cf3d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,37 +1,45 @@ import logging -from dsc.config import configure_logger, configure_sentry +import pytest -def test_configure_logger_not_verbose(): +def test_check_required_env_vars(monkeypatch, config_instance): + monkeypatch.delenv("WORKSPACE") + with pytest.raises(OSError, match="Missing required environment variables:"): + config_instance.check_required_env_vars() + + +def test_configure_logger_not_verbose(config_instance, stream): logger = logging.getLogger(__name__) - result = configure_logger(logger, verbose=False) - info_log_level = 20 - assert logger.getEffectiveLevel() == info_log_level + result = config_instance.configure_logger(logger, stream, verbose=False) + assert logger.getEffectiveLevel() == logging.INFO assert result == "Logger 'tests.test_config' configured with level=INFO" + stream.seek(0) + assert next(stream) == "INFO\n" -def test_configure_logger_verbose(): +def test_configure_logger_verbose(config_instance, stream): logger = logging.getLogger(__name__) - result = configure_logger(logger, verbose=True) - debug_log_level = 10 - assert logger.getEffectiveLevel() == debug_log_level + result = config_instance.configure_logger(logger, stream, verbose=True) + assert logger.getEffectiveLevel() == logging.DEBUG assert result == "Logger 'tests.test_config' configured with level=DEBUG" + stream.seek(0) + assert next(stream) == "DEBUG\n" -def test_configure_sentry_no_env_variable(monkeypatch): +def test_configure_sentry_no_env_variable(monkeypatch, config_instance): monkeypatch.delenv("SENTRY_DSN", raising=False) - result = configure_sentry() + result = config_instance.configure_sentry() assert result == "No Sentry DSN found, exceptions will not be sent to Sentry" -def test_configure_sentry_env_variable_is_none(monkeypatch): +def test_configure_sentry_env_variable_is_none(monkeypatch, config_instance): monkeypatch.setenv("SENTRY_DSN", "None") - result = configure_sentry() + result = config_instance.configure_sentry() assert result == "No Sentry DSN found, exceptions will not be sent to Sentry" -def test_configure_sentry_env_variable_is_dsn(monkeypatch): +def test_configure_sentry_env_variable_is_dsn(monkeypatch, config_instance): monkeypatch.setenv("SENTRY_DSN", "https://1234567890@00000.ingest.sentry.io/123456") - result = configure_sentry() + result = config_instance.configure_sentry() assert result == "Sentry DSN found, exceptions will be sent to Sentry with env=test" diff --git a/tests/test_s3.py b/tests/test_s3.py new file mode 100644 index 0000000..6c174b1 --- /dev/null +++ b/tests/test_s3.py @@ -0,0 +1,67 @@ +from http import HTTPStatus + +import pytest +from botocore.exceptions import ClientError + + +def test_s3_archive_file_in_bucket(mocked_s3, s3_client): + s3_client.put_file( + file_content="test1,test2,test3,test4", + bucket="awd", + key="test.csv", + ) + s3_client.archive_file_with_new_key( + bucket="awd", + key="test.csv", + archived_key_prefix="archived", + ) + with pytest.raises(ClientError) as e: + response = s3_client.client.get_object(Bucket="awd", Key="test.csv") + assert ( + "An error occurred (NoSuchKey) when calling the GetObject operation: The" + " specified key does not exist." in str(e.value) + ) + response = s3_client.client.get_object(Bucket="awd", Key="archived/test.csv") + assert response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK + + +def test_s3_put_file(mocked_s3, s3_client): + assert "Contents" not in s3_client.client.list_objects(Bucket="awd") + s3_client.put_file( + file_content=str({"metadata": {"key": "dc.title", "value": "A Title"}}), + bucket="awd", + key="test.json", + ) + assert len(s3_client.client.list_objects(Bucket="awd")["Contents"]) == 1 + assert ( + s3_client.client.list_objects(Bucket="awd")["Contents"][0]["Key"] == "test.json" + ) + + +def test_s3_get_files_iter_with_matching_csv(mocked_s3, s3_client): + s3_client.put_file( + file_content="test1,test2,test3,test4", + bucket="awd", + key="test.csv", + ) + assert list( + s3_client.get_files_iter( + bucket="awd", file_type="csv", excluded_key_prefix="archived" + ) + ) == ["test.csv"] + + +def test_s3_get_files_iter_without_matching_csv(mocked_s3, s3_client): + s3_client.put_file( + file_content="test1,test2,test3,test4", + bucket="awd", + key="archived/test.csv", + ) + assert ( + list( + s3_client.get_files_iter( + bucket="awd", file_type="csv", excluded_key_prefix="archived" + ) + ) + == [] + )