diff --git a/Pipfile.lock b/Pipfile.lock index 0252f65..e9c94c9 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,12 +18,12 @@ "default": { "boto3": { "hashes": [ - "sha256:473438feafe77d29fbea532a91a65de0d8751a4fa5822127218710a205e28e7a", - "sha256:ccb1a365d3084de53b58f8dfc056462f49b16931c139f4c8ac5f0bca8cb8fe81" + "sha256:a09871805f8e462349a1c33c23eb413668df0bf68424e61d53518e1a7d883b2f", + "sha256:cc819cdbccbc2d0dc185f1dcfe74cf3809489c4cae63c2e5d6a557aa0c5ab928" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.73" + "version": "==1.35.77" }, "boto3-stubs": { "extras": [ @@ -31,27 +31,27 @@ "ses" ], "hashes": [ - "sha256:b935f0b62be1e18445f63cd9f5bbb4fe9a792d99efa9eb7f37b641ed4a6e70e0", - "sha256:d1c072dfa59fbe0d91ba8e8966e844d9eb79ccc5f59e49914f796f29cd96a14d" + "sha256:2800b7b83c9c414ac33018eb19bf3ede9d026d9220e7916ff9e6e6343e848b87", + "sha256:e749e96a3c9908ef56a52a4c309c9c2c04ee44bd4f46491c0381b6bcb36320e9" ], "markers": "python_version >= '3.8'", - "version": "==1.35.73" + "version": "==1.35.77" }, "botocore": { "hashes": [ - "sha256:8a6a0f5ad119e38d850571df8c625dbad66aec1b20c15f84cdcb95258f9f1edb", - "sha256:b2e3ecdd1769f011f72c4c0d0094570ba125f4ca327f24269e4d68eb5d9878b9" + "sha256:17b778016644e9342ca3ff2f430c1d1db0c6126e9b41a57cff52ac58e7a455e0", + "sha256:3faa27d65841499762228902d7e215fa99a4c2fdc76c9113e1c3f339bdf685b8" ], "markers": "python_version >= '3.8'", - "version": "==1.35.73" + "version": "==1.35.77" }, "botocore-stubs": { "hashes": [ - "sha256:54f7bcc325382050ae6aa839163f93f5c4e777db9c0fd2da3ad0744720895fbe", - "sha256:e9a20b0a29621674b46225fdb88bf00a0bca5216413d717895b75ba2dd63c6cc" + "sha256:617508d023e0bc98901e0189b794c4b3f289c1747c7cc410173ad698c819a716", + "sha256:c977a049481d50a14bf2db0ef15020b76734ff628d4b8e0e77b8d1c65318369e" ], "markers": "python_version >= '3.8'", - "version": "==1.35.73" + "version": "==1.35.76" }, "certifi": { "hashes": [ @@ -87,17 +87,17 @@ }, "mypy-boto3-dynamodb": { "hashes": [ - "sha256:187915c781f352bc79d35b08a094605515ecc54f30107f629972c3358b864a5c", - "sha256:92eac35c49e9f3ff23a4ad6dee5dc54e410e0c49a98b4d93493c7000ebe74568" + "sha256:a815d044b8f5f4ba308ea3114916565fbd932fcaf218f8d0288b2840415f9c46", + "sha256:b693b459abb1910cbb28f3a478ced8c6e6515f1bf136b45aca1a76b6146b5adb" ], - "version": "==1.35.60" + "version": "==1.35.74" }, "mypy-boto3-ec2": { "hashes": [ - "sha256:3206cd6da473647cdefa5dcec4121b4a83778f49ee540ca4b8aeb6c337975b69", - "sha256:d2ff43ad1c42655cbcbb06d11dff74b3827503d80a99a78098ab52ba0fbb7235" + "sha256:b36c20b6878aad045df5fdf1de855850dbdfff7878af8381bb4af607d286428f", + "sha256:c3bd391f54c7c13c2e00591277f0edd5065a7448d268e11b8d8f569ae111250b" ], - "version": "==1.35.72" + "version": "==1.35.77" }, "mypy-boto3-lambda": { "hashes": [ @@ -115,10 +115,10 @@ }, "mypy-boto3-s3": { "hashes": [ - "sha256:571e659c1d355499d5e5070f33e613a1e251e6f5d2a57d535c5eaef52ebb6a86", - "sha256:b2a18ca57079659eb602dcfc4abb56425c793ccb1939826e401d4f2ddf9128b0" + "sha256:35f9ae109c3cb64ac6b44596dffc429058085ddb82f4daaf5be0a39e5cc1b576", + "sha256:6cf1f034985fe610754c3e6ef287490629870d508ada13b7d61e7b9aaeb46108" ], - "version": "==1.35.72" + "version": "==1.35.76" }, "mypy-boto3-ses": { "hashes": [ @@ -152,28 +152,28 @@ }, "sentry-sdk": { "hashes": [ - "sha256:7b0b3b709dee051337244a09a30dbf6e95afe0d34a1f8b430d45e0982a7c125b", - "sha256:ee4a4d2ae8bfe3cac012dcf3e4607975904c137e1738116549fc3dbbb6ff0e36" + "sha256:467df6e126ba242d39952375dd816fbee0f217d119bf454a8ce74cf1e7909e8d", + "sha256:ebdc08228b4d131128e568d696c210d846e5b9d70aa0327dec6b1272d9d40b84" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2.19.0" + "version": "==2.19.2" }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "version": "==1.17.0" }, "types-awscrt": { "hashes": [ - "sha256:0d362a5d62d68ca4216f458172f41c1123ec04791d68364de8ee8b61b528b262", - "sha256:a20b425dabb258bc3d07a5e7de503fd9558dd1542d72de796e74e402c6d493b2" + "sha256:043c0ae0fe5d272618294cbeaf1c349a654a9f7c00121be64d27486933ac4a26", + "sha256:cc0057885cb7ce1e66856123a4c2861b051e9f0716b1767ad72bfe4ca26bbcd4" ], "markers": "python_version >= '3.8'", - "version": "==0.23.1" + "version": "==0.23.3" }, "types-s3transfer": { "hashes": [ @@ -224,20 +224,20 @@ }, "boto3": { "hashes": [ - "sha256:473438feafe77d29fbea532a91a65de0d8751a4fa5822127218710a205e28e7a", - "sha256:ccb1a365d3084de53b58f8dfc056462f49b16931c139f4c8ac5f0bca8cb8fe81" + "sha256:a09871805f8e462349a1c33c23eb413668df0bf68424e61d53518e1a7d883b2f", + "sha256:cc819cdbccbc2d0dc185f1dcfe74cf3809489c4cae63c2e5d6a557aa0c5ab928" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.73" + "version": "==1.35.77" }, "botocore": { "hashes": [ - "sha256:8a6a0f5ad119e38d850571df8c625dbad66aec1b20c15f84cdcb95258f9f1edb", - "sha256:b2e3ecdd1769f011f72c4c0d0094570ba125f4ca327f24269e4d68eb5d9878b9" + "sha256:17b778016644e9342ca3ff2f430c1d1db0c6126e9b41a57cff52ac58e7a455e0", + "sha256:3faa27d65841499762228902d7e215fa99a4c2fdc76c9113e1c3f339bdf685b8" ], "markers": "python_version >= '3.8'", - "version": "==1.35.73" + "version": "==1.35.77" }, "certifi": { "hashes": [ @@ -453,71 +453,71 @@ "toml" ], "hashes": [ - "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" + "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4", + "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c", + "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f", + "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b", + "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6", + "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae", + "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692", + "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4", + "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4", + "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717", + "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d", + "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198", + "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1", + "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3", + "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb", + "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d", + "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08", + "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf", + "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b", + "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710", + "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c", + "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae", + "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077", + "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00", + "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb", + "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664", + "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014", + "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9", + "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6", + "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e", + "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9", + "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa", + "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611", + "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b", + "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a", + "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8", + "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030", + "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678", + "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015", + "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902", + "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97", + "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845", + "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419", + "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464", + "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be", + "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9", + "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7", + "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be", + "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1", + "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba", + "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5", + "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073", + "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4", + "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a", + "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a", + "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3", + "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599", + "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0", + "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b", + "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec", + "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1", + "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3" ], "markers": "python_version >= '3.9'", - "version": "==7.6.8" + "version": "==7.6.9" }, "coveralls": { "hashes": [ @@ -898,28 +898,28 @@ }, "ruff": { "hashes": [ - "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" + "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f", + "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea", + "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248", + "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d", + "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f", + "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29", + "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22", + "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0", + "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1", + "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58", + "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5", + "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d", + "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897", + "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa", + "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93", + "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5", + "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c", + "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.8.1" + "version": "==0.8.2" }, "s3transfer": { "hashes": [ @@ -931,11 +931,11 @@ }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "version": "==1.17.0" }, "typing-extensions": { "hashes": [ diff --git a/dsc/exceptions.py b/dsc/exceptions.py new file mode 100644 index 0000000..873432a --- /dev/null +++ b/dsc/exceptions.py @@ -0,0 +1,2 @@ +class InvalidSQSMessageError(Exception): + pass diff --git a/dsc/utilities/__init__.py b/dsc/utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dsc/utilities/aws/__init__.py b/dsc/utilities/aws/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dsc/s3.py b/dsc/utilities/aws/s3.py similarity index 100% rename from dsc/s3.py rename to dsc/utilities/aws/s3.py diff --git a/dsc/utilities/aws/ses.py b/dsc/utilities/aws/ses.py new file mode 100644 index 0000000..8c69d83 --- /dev/null +++ b/dsc/utilities/aws/ses.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import logging +from email.mime.application import MIMEApplication +from email.mime.multipart import MIMEMultipart +from typing import TYPE_CHECKING + +from boto3 import client + +if TYPE_CHECKING: + from mypy_boto3_ses.type_defs import SendRawEmailResponseTypeDef + +logger = logging.getLogger(__name__) + + +class SESClient: + """A class to perform common SES operations for this application.""" + + def __init__(self, region: str) -> None: + self.client = client("ses", region_name=region) + + def create_and_send_email( + self, + subject: str, + attachment_content: str, + attachment_name: str, + source_email_address: str, + recipient_email_address: str, + ) -> None: + """Create an email message and send it via SES. + + Args: + subject: The subject of the email. + attachment_content: The content of the email attachment. + attachment_name: The name of the email attachment. + source_email_address: The email address of the sender. + recipient_email_address: The email address of the receipient. + """ + message = self._create_email(subject, attachment_content, attachment_name) + self._send_email(source_email_address, recipient_email_address, message) + logger.debug(f"Logs sent to {recipient_email_address}") + + def _create_email( + self, + subject: str, + attachment_content: str, + attachment_name: str, + ) -> MIMEMultipart: + """Create an email. + + Args: + subject: The subject of the email. + attachment_content: The content of the email attachment. + attachment_name: The name of the email attachment. + """ + message = MIMEMultipart() + message["Subject"] = subject + attachment_object = MIMEApplication(attachment_content) + attachment_object.add_header( + "Content-Disposition", "attachment", filename=attachment_name + ) + message.attach(attachment_object) + return message + + def _send_email( + self, + source_email_address: str, + recipient_email_address: str, + message: MIMEMultipart, + ) -> SendRawEmailResponseTypeDef: + """Send email via SES. + + Args: + source_email_address: The email address of the sender. + recipient_email_address: The email address of the receipient. + message: The message to be sent. + """ + return self.client.send_raw_email( + Source=source_email_address, + Destinations=[recipient_email_address], + RawMessage={ + "Data": message.as_string(), + }, + ) diff --git a/dsc/utilities/aws/sqs.py b/dsc/utilities/aws/sqs.py new file mode 100644 index 0000000..a6c0a06 --- /dev/null +++ b/dsc/utilities/aws/sqs.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +import json +import logging +from typing import TYPE_CHECKING, Any + +from boto3 import client + +from dsc.exceptions import InvalidSQSMessageError + +if TYPE_CHECKING: + from collections.abc import Iterator, Mapping + + from mypy_boto3_sqs.type_defs import ( + EmptyResponseMetadataTypeDef, + MessageAttributeValueTypeDef, + MessageTypeDef, + SendMessageResultTypeDef, + ) + +logger = logging.getLogger(__name__) + + +class SQSClient: + """A class to perform common SQS operations for this application.""" + + def __init__( + self, region: str, queue_name: str, queue_url: str | None = None + ) -> None: + self.client = client("sqs", region_name=region) + self.queue_name = queue_name + self._queue_url: str | None = queue_url + + @property + def queue_url(self) -> str: + """Property to provide QueueUrl, caching it for reuse.""" + if not self._queue_url: + self._queue_url = self.get_queue_url() + return self._queue_url + + def get_queue_url(self) -> str: + """Get SQS queue URL from name.""" + return self.client.get_queue_url(QueueName=self.queue_name)["QueueUrl"] + + @staticmethod + def create_dss_message_attributes( + package_id: str, submission_source: str, output_queue: str + ) -> dict[str, Any]: + """Create attributes for a DSpace Submission Service message. + + Args: + package_id: The PackageID field which is populated by the submission's + identifier. + submission_source: The source for the submission. + output_queue: The SQS output queue used for retrieving result messages. + """ + return { + "PackageID": {"DataType": "String", "StringValue": package_id}, + "SubmissionSource": {"DataType": "String", "StringValue": submission_source}, + "OutputQueue": {"DataType": "String", "StringValue": output_queue}, + } + + @staticmethod + def create_dss_message_body( + submission_system: str, + collection_handle: str, + metadata_s3_uri: str, + bitstream_file_name: str, + bitstream_s3_uri: str, + ) -> str: + """Create body for a DSpace Submission Service message. + + Args: + submission_system: The system where the article is uploaded. + collection_handle: The handle of collection where the article is uploaded. + metadata_s3_uri: The S3 URI for the metadata JSON file. + bitstream_file_name: The file name for the article content which is uploaded as a + bitstream. + bitstream_s3_uri: The S3 URI for the article content file. + """ + return json.dumps( + { + "SubmissionSystem": submission_system, + "CollectionHandle": collection_handle, + "MetadataLocation": metadata_s3_uri, + "Files": [ + { + "BitstreamName": bitstream_file_name, + "FileLocation": bitstream_s3_uri, + "BitstreamDescription": None, + } + ], + } + ) + + def delete(self, receipt_handle: str) -> EmptyResponseMetadataTypeDef: + """Delete message from SQS queue. + + Args: + receipt_handle: The receipt handle of the message to be deleted. + """ + logger.debug("Deleting '{receipt_handle}' from SQS queue: {self.queue_name}") + response = self.client.delete_message( + QueueUrl=self.queue_url, + ReceiptHandle=receipt_handle, + ) + logger.debug(f"Message deleted from SQS queue: {response}") + + return response + + def process_result_message(self, sqs_message: MessageTypeDef) -> tuple[str, str]: + """Validate, extract data, and delete an SQS result message. + + Args: + sqs_message: An SQS result message to be processed. + """ + self.validate_message(sqs_message) + identifier = sqs_message["MessageAttributes"]["PackageID"]["StringValue"] + message_body = json.loads(str(sqs_message["Body"])) + self.delete(sqs_message["ReceiptHandle"]) + return identifier, message_body + + def receive(self) -> Iterator[MessageTypeDef]: + """Receive messages from SQS queue.""" + logger.debug(f"Receiving messages from SQS queue: {self.queue_name}") + while True: + response = self.client.receive_message( + QueueUrl=self.queue_url, + MaxNumberOfMessages=10, + MessageAttributeNames=["All"], + ) + if "Messages" in response: + for message in response["Messages"]: + logger.debug( + f"Message retrieved from SQS queue {self.queue_name}: {message}" + ) + yield message + else: + logger.debug(f"No more messages from SQS queue: {self.queue_name}") + break + + def send( + self, + message_attributes: Mapping[str, MessageAttributeValueTypeDef], + message_body: str, + ) -> SendMessageResultTypeDef: + """Send message via SQS. + + Args: + message_attributes: The attributes of the message to send. + message_body: The body of the message to send. + """ + logger.debug(f"Sending message to SQS queue: {self.queue_name}") + response = self.client.send_message( + QueueUrl=self.queue_url, + MessageAttributes=message_attributes, + MessageBody=str(message_body), + ) + logger.debug(f"Response from SQS queue: {response}") + return response + + def validate_message(self, sqs_message: MessageTypeDef) -> None: + """Validate that an SQS message is formatted as expected. + + Args: + sqs_message: An SQS message to be evaluated. + + """ + if not sqs_message.get("ReceiptHandle"): + raise InvalidSQSMessageError( + f"Failed to retrieve 'ReceiptHandle' from message: {sqs_message}" + ) + self.validate_message_attributes(sqs_message=sqs_message) + self.validate_message_body(sqs_message=sqs_message) + + @staticmethod + def validate_message_attributes(sqs_message: MessageTypeDef) -> None: + """Validate that "MessageAttributes" field is formatted as expected. + + Args: + sqs_message: An SQS message to be evaluated. + """ + if ( + "MessageAttributes" not in sqs_message + or "PackageID" not in sqs_message["MessageAttributes"] + or not sqs_message["MessageAttributes"]["PackageID"].get("StringValue") + ): + raise InvalidSQSMessageError( + f"Failed to parse SQS message attributes: {sqs_message}" + ) + + @staticmethod + def validate_message_body(sqs_message: MessageTypeDef) -> None: + """Validate that "Body" field is formatted as expected. + + Args: + sqs_message: An SQS message to be evaluated. + """ + if "Body" not in sqs_message or not json.loads(str(sqs_message["Body"])): + raise InvalidSQSMessageError( + f"Failed to parse SQS message body: {sqs_message}" + ) diff --git a/pyproject.toml b/pyproject.toml index e0345ea..6f318b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ ignore = [ "D103", "D104", "D107", + "EM102", "G004", "N812", "PLR0912", @@ -43,7 +44,8 @@ ignore = [ "PLR0915", "PTH", "S320", - "S321", + "S321", + "TRY003", ] # allow autofix behavior for specified rules diff --git a/tests/conftest.py b/tests/conftest.py index 00105c2..189390e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import json from io import StringIO import boto3 @@ -6,7 +7,9 @@ from moto import mock_aws from dsc.config import Config -from dsc.s3 import S3Client +from dsc.utilities.aws.s3 import S3Client +from dsc.utilities.aws.ses import SESClient +from dsc.utilities.aws.sqs import SQSClient @pytest.fixture(autouse=True) @@ -14,6 +17,8 @@ def _test_env(monkeypatch): monkeypatch.setenv("SENTRY_DSN", "None") monkeypatch.setenv("WORKSPACE", "test") monkeypatch.setenv("AWS_REGION_NAME", "us-east-1") + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") @pytest.fixture @@ -29,6 +34,59 @@ def mocked_s3(config_instance): yield s3 +@pytest.fixture +def mocked_ses(config_instance): + with mock_aws(): + ses = boto3.client("ses", region_name=config_instance.AWS_REGION_NAME) + ses.verify_email_identity(EmailAddress="noreply@example.com") + yield ses + + +@pytest.fixture +def mocked_sqs_input(sqs_client, config_instance): + with mock_aws(): + sqs = boto3.resource("sqs", region_name=config_instance.AWS_REGION_NAME) + sqs.create_queue(QueueName="mock-input-queue") + yield sqs + + +@pytest.fixture +def mocked_sqs_output(): + with mock_aws(): + sqs = boto3.resource("sqs", region_name="us-east-1") + sqs.create_queue(QueueName="mock-output-queue") + yield sqs + + +@pytest.fixture +def result_message_attributes(): + return { + "PackageID": {"DataType": "String", "StringValue": "10.1002/term.3131"}, + "SubmissionSource": {"DataType": "String", "StringValue": "Submission system"}, + } + + +@pytest.fixture +def result_message_body(): + return json.dumps( + { + "ResultType": "success", + "ItemHandle": "1721.1/131022", + "lastModified": "Thu Sep 09 17:56:39 UTC 2021", + "Bitstreams": [ + { + "BitstreamName": "10.1002-term.3131.pdf", + "BitstreamUUID": "a1b2c3d4e5", + "BitstreamChecksum": { + "value": "a4e0f4930dfaff904fa3c6c85b0b8ecc", + "checkSumAlgorithm": "MD5", + }, + } + ], + } + ) + + @pytest.fixture def runner(): return CliRunner() @@ -39,6 +97,55 @@ def s3_client(): return S3Client() +@pytest.fixture +def ses_client(config_instance): + return SESClient(region=config_instance.AWS_REGION_NAME) + + +@pytest.fixture +def sqs_client(config_instance): + return SQSClient( + region=config_instance.AWS_REGION_NAME, + queue_name="mock-output-queue", + ) + + @pytest.fixture def stream(): return StringIO() + + +@pytest.fixture +def submission_message_attributes(): + return { + "PackageID": {"DataType": "String", "StringValue": "123"}, + "SubmissionSource": {"DataType": "String", "StringValue": "Submission system"}, + "OutputQueue": {"DataType": "String", "StringValue": "DSS queue"}, + } + + +@pytest.fixture +def submission_message_body(): + return json.dumps( + { + "SubmissionSystem": "DSpace@MIT", + "CollectionHandle": "123.4/5678", + "MetadataLocation": "s3://awd/10.1002-term.3131.json", + "Files": [ + { + "BitstreamName": "10.1002-term.3131.pdf", + "FileLocation": "s3://awd/10.1002-term.3131.pdf", + "BitstreamDescription": None, + } + ], + } + ) + + +@pytest.fixture +def result_message_valid(result_message_attributes, result_message_body): + return { + "ReceiptHandle": "lvpqxcxlmyaowrhbvxadosldaghhidsdralddmejhdrnrfeyfuphzs", + "Body": result_message_body, + "MessageAttributes": result_message_attributes, + } diff --git a/tests/test_ses.py b/tests/test_ses.py new file mode 100644 index 0000000..87d7e45 --- /dev/null +++ b/tests/test_ses.py @@ -0,0 +1,35 @@ +import logging +from email.mime.multipart import MIMEMultipart +from http import HTTPStatus + + +def test_ses_create_and_send_email(caplog, mocked_ses, ses_client): + with caplog.at_level(logging.DEBUG): + ses_client.create_and_send_email( + subject="Email subject", + attachment_content="", + attachment_name="attachment", + source_email_address="noreply@example.com", + recipient_email_address="test@example.com", + ) + assert "Logs sent to test@example.com" in caplog.text + + +def test_ses_create_email(ses_client): + message = ses_client._create_email( # noqa: SLF001 + subject="Email subject", + attachment_content="", + attachment_name="attachment", + ) + assert message["Subject"] == "Email subject" + assert message.get_payload()[0].get_filename() == "attachment" + + +def test_ses_send_email(mocked_ses, ses_client): + message = MIMEMultipart() + response = ses_client._send_email( # noqa: SLF001 + source_email_address="noreply@example.com", + recipient_email_address="test@example.com", + message=message, + ) + assert response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK diff --git a/tests/test_sqs.py b/tests/test_sqs.py new file mode 100644 index 0000000..f826b7b --- /dev/null +++ b/tests/test_sqs.py @@ -0,0 +1,166 @@ +from http import HTTPStatus + +import pytest +from botocore.exceptions import ClientError + +from dsc.exceptions import InvalidSQSMessageError + + +def test_sqs_create_dss_message_attributes(sqs_client, submission_message_attributes): + dss_message_attributes = sqs_client.create_dss_message_attributes( + package_id="123", submission_source="Submission system", output_queue="DSS queue" + ) + assert dss_message_attributes == submission_message_attributes + + +def test_sqs_create_dss_message_body(sqs_client, submission_message_body): + dss_message_body = sqs_client.create_dss_message_body( + submission_system="DSpace@MIT", + collection_handle="123.4/5678", + metadata_s3_uri="s3://awd/10.1002-term.3131.json", + bitstream_file_name="10.1002-term.3131.pdf", + bitstream_s3_uri="s3://awd/10.1002-term.3131.pdf", + ) + assert dss_message_body == submission_message_body + + +def test_sqs_delete_nonexistent_message_raises_error(mocked_sqs_output, sqs_client): + with pytest.raises(ClientError): + sqs_client.delete(receipt_handle="12345678") + + +def test_sqs_delete_success( + mocked_sqs_output, + sqs_client, + result_message_attributes, + result_message_body, +): + sqs_client.send( + message_attributes=result_message_attributes, + message_body=result_message_body, + ) + messages = sqs_client.receive() + receipt_handle = next(messages)["ReceiptHandle"] + response = sqs_client.delete(receipt_handle=receipt_handle) + assert response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK + + +def test_sqs_process_result_message( + mocked_sqs_output, + sqs_client, + result_message_attributes, + result_message_body, +): + sqs_client.send( + message_attributes=result_message_attributes, + message_body=result_message_body, + ) + messages = sqs_client.receive() + identifier, message_body = sqs_client.process_result_message( + sqs_message=next(messages) + ) + assert identifier == "10.1002/term.3131" + assert message_body == { + "Bitstreams": [ + { + "BitstreamChecksum": { + "checkSumAlgorithm": "MD5", + "value": "a4e0f4930dfaff904fa3c6c85b0b8ecc", + }, + "BitstreamName": "10.1002-term.3131.pdf", + "BitstreamUUID": "a1b2c3d4e5", + } + ], + "ItemHandle": "1721.1/131022", + "ResultType": "success", + "lastModified": "Thu Sep 09 17:56:39 UTC 2021", + } + + +def test_sqs_process_result_message_raises_invalid_sqs_exception( + mocked_sqs_output, + sqs_client, +): + sqs_client.send(message_attributes={}, message_body={}) + messages = sqs_client.receive() + with pytest.raises(InvalidSQSMessageError): + sqs_client.process_result_message( + sqs_message=next(messages), + ) + + +def test_sqs_receive_raises_error_for_incorrect_queue(mocked_sqs_output, sqs_client): + sqs_client.queue_name = "non-existent" + with pytest.raises(ClientError): + next(sqs_client.receive()) + + +def test_sqs_receive_success( + mocked_sqs_output, + sqs_client, + result_message_attributes, + result_message_body, +): + sqs_client.send( + message_attributes=result_message_attributes, + message_body=result_message_body, + ) + messages = sqs_client.receive() + for message in messages: + assert message["Body"] == str(result_message_body) + assert message["MessageAttributes"] == result_message_attributes + + +def test_sqs_send_raises_error_for_incorrect_queue( + mocked_sqs_input, sqs_client, submission_message_attributes, submission_message_body +): + sqs_client.queue_name = "non-existent" + with pytest.raises(ClientError): + sqs_client.send( + message_attributes=submission_message_attributes, + message_body=submission_message_body, + ) + + +def test_sqs_send_success( + mocked_sqs_input, sqs_client, submission_message_attributes, submission_message_body +): + sqs_client.queue_name = "mock-input-queue" + response = sqs_client.send( + message_attributes=submission_message_attributes, + message_body=submission_message_body, + ) + assert response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK + + +def test_sqs_validate_message_no_receipthandle_invalid( + mocked_sqs_input, sqs_client, result_message_valid +): + with pytest.raises(InvalidSQSMessageError): + sqs_client.validate_message(sqs_message={}) + + +def test_sqs_validate_message_valid(mocked_sqs_input, sqs_client, result_message_valid): + assert not sqs_client.validate_message(sqs_message=result_message_valid) + + +def test_sqs_validate_message_attributes_invalid(mocked_sqs_input, sqs_client): + with pytest.raises(InvalidSQSMessageError): + sqs_client.validate_message_attributes(sqs_message={}) + + +def test_sqs_validate_message_attributes_valid( + mocked_sqs_input, sqs_client, result_message_valid +): + assert not sqs_client.validate_message_attributes(sqs_message=result_message_valid) + + +def test_sqs_validate_message_body_invalid(caplog, mocked_sqs_input, sqs_client): + with pytest.raises(InvalidSQSMessageError): + sqs_client.validate_message_body(sqs_message={}) + + +def test_sqs_validate_message_body_valid( + mocked_sqs_input, sqs_client, result_message_valid +): + assert not sqs_client.validate_message_body(sqs_message=result_message_valid)