diff --git a/Pipfile b/Pipfile index a710c27..12d1d12 100644 --- a/Pipfile +++ b/Pipfile @@ -14,6 +14,10 @@ httpx = "*" kaggle = ">=1.5.16" prefect-slack = "*" tabulate = "==0.9.0" +sqlmodel = ">=0.0.14" +prefect-sqlalchemy = "*" +mysql-connector-python = "*" +pydantic-settings = "*" [dev-packages] pipenv = "*" diff --git a/Pipfile.lock b/Pipfile.lock index c053f42..38240af 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "af57dc2838edb4cfe0b14a4f31d272afdfa6ad8846f8bd99a892c8920d9ce2a9" + "sha256": "6c5c734af3606d1ffc2c309358950190f60fc4b1660dc92576aa67e2fd204250" }, "pipfile-spec": 6, "requires": { @@ -236,19 +236,19 @@ }, "boto3": { "hashes": [ - "sha256:33a8b6d9136fa7427160edb92d2e50f2035f04e9d63a2d1027349053e12626aa", - "sha256:b2f321e20966f021ec800b7f2c01287a3dd04fc5965acdfbaa9c505a24ca45d1" + "sha256:35bcbecf1b5d3620c93f0062d2994177f8bda25a9d2cba144d6462793c16065b", + "sha256:476896e70d36c9134d4125834280c597c17b54bff4902baf2e5fcde74f8acec8" ], "markers": "python_version >= '3.8'", - "version": "==1.34.34" + "version": "==1.34.39" }, "botocore": { "hashes": [ - "sha256:54093dc97372bb7683f5c61a279aa8240408abf3b2cc494ae82a9a90c1b784b5", - "sha256:cd060b0d88ebb2b893f1411c1db7f2ba66cc18e52dcc57ad029564ef5fec437b" + "sha256:9f00bd5e4698bcdd37ce6e224a896baf58d209678ed92834944b767de9061cc5", + "sha256:e175360445424b83b0e28ae20d301b99cf44ff2c9d5ab1d8670899bec05a9753" ], "markers": "python_version >= '3.8'", - "version": "==1.34.34" + "version": "==1.34.39" }, "cachetools": { "hashes": [ @@ -613,11 +613,11 @@ }, "fsspec": { "hashes": [ - "sha256:8548d39e8810b59c38014934f6b31e57f40c1b20f911f4cc2b85389c7e9bf0cb", - "sha256:d800d87f72189a745fa3d6b033b9dc4a34ad069f60ca60b943a63599f5501960" + "sha256:817f969556fa5916bc682e02ca2045f96ff7f586d45110fcb76022063ad2c7d8", + "sha256:b6ad1a679f760dda52b1168c859d01b7b80648ea6f7f7c7f5a8a91dc3f3ecb84" ], "markers": "python_version >= '3.8'", - "version": "==2023.12.2" + "version": "==2024.2.0" }, "google-auth": { "hashes": [ @@ -700,11 +700,11 @@ }, "griffe": { "hashes": [ - "sha256:76c4439eaa2737af46ae003c331ab6ca79c5365b552f7b5aed263a3b4125735b", - "sha256:db1da6d1d8e08cbb20f1a7dee8c09da940540c2d4c1bfa26a9091cf6fc36a9ec" + "sha256:5b8c023f366fe273e762131fe4bfd141ea56c09b3cb825aa92d06a82681cfd93", + "sha256:66c48a62e2ce5784b6940e603300fcfb807b6f099b94e7f753f1841661fd5c7c" ], "markers": "python_version >= '3.8'", - "version": "==0.40.0" + "version": "==0.40.1" }, "h11": { "hashes": [ @@ -815,10 +815,10 @@ }, "kaggle": { "hashes": [ - "sha256:6a91a7bacb461d2682edf4ca7163f1811ed8ca8a84492263c468ffe7703f101f" + "sha256:9bf8b6d76a23bcbc327f15cc21fc678baa733953dce012ec795c274cfae745d0" ], "index": "pypi", - "version": "==1.6.4" + "version": "==1.6.5" }, "kubernetes": { "hashes": [ @@ -1123,6 +1123,39 @@ "markers": "python_version >= '3.8'", "version": "==1.34.17" }, + "mysql-connector-python": { + "hashes": [ + "sha256:0deb38f05057e12af091a48e03a1ff00e213945880000f802879fae5665e7502", + "sha256:125714c998a697592bc56cce918a1acc58fadc510a7f588dbef3e53a1920e086", + "sha256:1db5b48b4ff7d24344217ed2418b162c7677eec86ab9766dc0e5feae39c90974", + "sha256:201e609159b84a247be87b76f5deb79e8c6b368e91f043790e62077f13f3fed8", + "sha256:27f8be2087627366a44a6831ec68b568c98dbf0f4ceff24682d90c21db6e0f1f", + "sha256:4be4165e4cd5acb4659261ddc74e9164d2dfa0d795d5695d52f2bf39ea0762fa", + "sha256:51d97bf771519829797556718d81e8b9bdcd0a00427740ca57c085094c8bde17", + "sha256:55cb57d8098c721abce20fdef23232663977c0e5c87a4d0f9f73466f32c7d168", + "sha256:5718e426cf67f041772d4984f709052201883f74190ba6feaddce5cbd3b99e6f", + "sha256:5e2c86c60be08c71bae755d811fe8b89ec4feb8117ec3440ebc6c042dd6f06bc", + "sha256:5f707a9b040ad4700fc447ba955c78b08f2dd5affde37ac2401918f7b6daaba3", + "sha256:73ee8bc5f9626c42b37342a91a825cddb3461f6bfbbd6524d8ccfd3293aaa088", + "sha256:77bae496566d3da77bb0e938d89243103d20ee41633f626a47785470451bf45c", + "sha256:7f4f5fa844c19ee3a78c4606f6e138b06829e75469592d90246a290c7befc322", + "sha256:85fa878fdd6accaeb7d609bd2637c2cfa61592e7f9bdbdc0da18b2fa998d3d5a", + "sha256:9302d774025e76a0fac46bfeea8854b3d6819715a6a16ff23bfcda04218a76b7", + "sha256:b2901391b651d60dab3cc8985df94976fc1ea59fa7324c5b19d0a4177914c8dd", + "sha256:c57d02fd6c28be444487e7905ede09e3fecb18377cf82908ca262826369d3401", + "sha256:de0f2f2baa9e091ca8bdc4a091f874f9cd0b84b256389596adb0e032a05fe9f9", + "sha256:de5c3ee89d9276356f93df003949d3ba4c486f32fec9ec9fd7bc0caab124d89c", + "sha256:de74055944b214bff56e1752ec213d705c421414c67a250fb695af0c5c214135", + "sha256:e4ff23aa8036b4c5b6463fa81398bb5a528a29f99955de6ba937f0bba57a2fe3", + "sha256:e868ccc7ad9fbc242546db04673d89cee87d12b8139affd114524553df4e5d6a", + "sha256:ec6dc3434a7deef74ab04e8978f6c5e181866a5423006c1b5aec5390a189d28d", + "sha256:f4ee7e07cca6b744874d60d6b0b24817d9246eb4e8d7269b7ddbe68763a0bd13", + "sha256:f7acacdf9fd4260702f360c00952ad9a9cc73e8b7475e0d0c973c085a3dd7b7d" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==8.3.0" + }, "oauthlib": { "hashes": [ "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", @@ -1133,59 +1166,59 @@ }, "orjson": { "hashes": [ - "sha256:03ea7ee7e992532c2f4a06edd7ee1553f0644790553a118e003e3c405add41fa", - "sha256:06e42e899dde61eb1851a9fad7f1a21b8e4be063438399b63c07839b57668f6c", - "sha256:09d60450cda3fa6c8ed17770c3a88473a16460cd0ff2ba74ef0df663b6fd3bb8", - "sha256:0fc156fba60d6b50743337ba09f052d8afc8b64595112996d22f5fce01ab57da", - "sha256:12756a108875526b76e505afe6d6ba34960ac6b8c5ec2f35faf73ef161e97e07", - "sha256:1bb8f657c39ecdb924d02e809f992c9aafeb1ad70127d53fb573a6a6ab59d549", - "sha256:2849f88a0a12b8d94579b67486cbd8f3a49e36a4cb3d3f0ab352c596078c730c", - "sha256:29bf08e2eadb2c480fdc2e2daae58f2f013dff5d3b506edd1e02963b9ce9f8a9", - "sha256:2dfaf71499d6fd4153f5c86eebb68e3ec1bf95851b030a4b55c7637a37bbdee4", - "sha256:3186b18754befa660b31c649a108a915493ea69b4fc33f624ed854ad3563ac65", - "sha256:410f24309fbbaa2fab776e3212a81b96a1ec6037259359a32ea79fbccfcf76aa", - "sha256:4a0cd56e8ee56b203abae7d482ac0d233dbfb436bb2e2d5cbcb539fe1200a312", - "sha256:54071b7398cd3f90e4bb61df46705ee96cb5e33e53fc0b2f47dbd9b000e238e1", - "sha256:5586a533998267458fad3a457d6f3cdbddbcce696c916599fa8e2a10a89b24d3", - "sha256:59feb148392d9155f3bfed0a2a3209268e000c2c3c834fb8fe1a6af9392efcbf", - "sha256:5c157e999e5694475a5515942aebeed6e43f7a1ed52267c1c93dcfde7d78d421", - "sha256:61563d5d3b0019804d782137a4f32c72dc44c84e7d078b89d2d2a1adbaa47b52", - "sha256:640e2b5d8e36b970202cfd0799d11a9a4ab46cf9212332cd642101ec952df7c8", - "sha256:6492ff5953011e1ba9ed1bf086835fd574bd0a3cbe252db8e15ed72a30479081", - "sha256:659a8d7279e46c97661839035a1a218b61957316bf0202674e944ac5cfe7ed83", - "sha256:67426651faa671b40443ea6f03065f9c8e22272b62fa23238b3efdacd301df31", - "sha256:6b4e2bed7d00753c438e83b613923afdd067564ff7ed696bfe3a7b073a236e07", - "sha256:890e7519c0c70296253660455f77e3a194554a3c45e42aa193cdebc76a02d82b", - "sha256:950951799967558c214cd6cceb7ceceed6f81d2c3c4135ee4a2c9c69f58aa225", - "sha256:96e44b21fe407b8ed48afbb3721f3c8c8ce17e345fbe232bd4651ace7317782d", - "sha256:975e72e81a249174840d5a8df977d067b0183ef1560a32998be340f7e195c730", - "sha256:99e8cd005b3926c3db9b63d264bd05e1bf4451787cc79a048f27f5190a9a0311", - "sha256:a2b6f5252c92bcab3b742ddb3ac195c0fa74bed4319acd74f5d54d79ef4715dc", - "sha256:a4ae815a172a1f073b05b9e04273e3b23e608a0858c4e76f606d2d75fcabde0c", - "sha256:a84a0c3d4841a42e2571b1c1ead20a83e2792644c5827a606c50fc8af7ca4bee", - "sha256:ab8add018a53665042a5ae68200f1ad14c7953fa12110d12d41166f111724656", - "sha256:af17fa87bccad0b7f6fd8ac8f9cbc9ee656b4552783b10b97a071337616db3e4", - "sha256:b0e9d73cdbdad76a53a48f563447e0e1ce34bcecef4614eb4b146383e6e7d8c9", - "sha256:b159baecfda51c840a619948c25817d37733a4d9877fea96590ef8606468b362", - "sha256:bc82a4db9934a78ade211cf2e07161e4f068a461c1796465d10069cb50b32a80", - "sha256:bd1b8ec63f0bf54a50b498eedeccdca23bd7b658f81c524d18e410c203189365", - "sha256:c95488e4aa1d078ff5776b58f66bd29d628fa59adcb2047f4efd3ecb2bd41a71", - "sha256:cbbf313c9fb9d4f6cf9c22ced4b6682230457741daeb3d7060c5d06c2e73884a", - "sha256:cbd0f3555205bf2a60f8812133f2452d498dbefa14423ba90fe89f32276f7abf", - "sha256:cd52dec9eddf4c8c74392f3fd52fa137b5f2e2bed1d9ae958d879de5f7d7cded", - "sha256:cfdaede0fa5b500314ec7b1249c7e30e871504a57004acd116be6acdda3b8ab3", - "sha256:d3cfb76600c5a1e6be91326b8f3b83035a370e727854a96d801c1ea08b708073", - "sha256:d664880d7f016efbae97c725b243b33c2cbb4851ddc77f683fd1eec4a7894146", - "sha256:d6ce2062c4af43b92b0221ed4f445632c6bf4213f8a7da5396a122931377acd9", - "sha256:da908d23a3b3243632b523344403b128722a5f45e278a8343c2bb67538dff0e4", - "sha256:daa438bd8024e03bcea2c5a92cd719a663a58e223fba967296b6ab9992259dbf", - "sha256:dde1bc7c035f2d03aa49dc8642d9c6c9b1a81f2470e02055e76ed8853cfae0c3", - "sha256:e773f251258dd82795fd5daeac081d00b97bacf1548e44e71245543374874bcf", - "sha256:ed398f9a9d5a1bf55b6e362ffc80ac846af2122d14a8243a1e6510a4eabcb71e", - "sha256:f4098c7674901402c86ba6045a551a2ee345f9f7ed54eeffc7d86d155c8427e5" + "sha256:031df1026c7ea8303332d78711f180231e3ae8b564271fb748a03926587c5546", + "sha256:0d3ba9d88e20765335260d7b25547d7c571eee2b698200f97afa7d8c7cd668fc", + "sha256:0d691c44604941945b00e0a13b19a7d9c1a19511abadf0080f373e98fdeb6b31", + "sha256:0fd9a2101d04e85086ea6198786a3f016e45475f800712e6833e14bf9ce2832f", + "sha256:16946d095212a3dec552572c5d9bca7afa40f3116ad49695a397be07d529f1fa", + "sha256:1ab9dbdec3f13f3ea6f937564ce21651844cfbf2725099f2f490426acf683c23", + "sha256:23f21faf072ed3b60b5954686f98157e073f6a8068eaa58dbde83e87212eda84", + "sha256:266e55c83f81248f63cc93d11c5e3a53df49a5d2598fa9e9db5f99837a802d5d", + "sha256:2cc03a35bfc71c8ebf96ce49b82c2a7be6af4b3cd3ac34166fdb42ac510bbfff", + "sha256:2f37f0cdd026ef777a4336e599d8194c8357fc14760c2a5ddcfdf1965d45504b", + "sha256:31372ba3a9fe8ad118e7d22fba46bbc18e89039e3bfa89db7bc8c18ee722dca8", + "sha256:31fb66b41fb2c4c817d9610f0bc7d31345728d7b5295ac78b63603407432a2b2", + "sha256:3869d65561f10071d3e7f35ae58fd377056f67d7aaed5222f318390c3ad30339", + "sha256:3deadd8dc0e9ff844b5b656fa30a48dbee1c3b332d8278302dd9637f6b09f627", + "sha256:43fd6036b16bb6742d03dae62f7bdf8214d06dea47e4353cde7e2bd1358d186f", + "sha256:446d9ad04204e79229ae19502daeea56479e55cbc32634655d886f5a39e91b44", + "sha256:4584e8eb727bc431baaf1bf97e35a1d8a0109c924ec847395673dfd5f4ef6d6f", + "sha256:49b7e3fe861cb246361825d1a238f2584ed8ea21e714bf6bb17cebb86772e61c", + "sha256:5b98cd948372f0eb219bc309dee4633db1278687161e3280d9e693b6076951d2", + "sha256:5ef58869f3399acbbe013518d8b374ee9558659eef14bca0984f67cb1fbd3c37", + "sha256:60da7316131185d0110a1848e9ad15311e6c8938ee0b5be8cbd7261e1d80ee8f", + "sha256:62e9a99879c4d5a04926ac2518a992134bfa00d546ea5a4cae4b9be454d35a22", + "sha256:63ef57a53bfc2091a7cd50a640d9ae866bd7d92a5225a1bab6baa60ef62583f2", + "sha256:6e47153db080f5e87e8ba638f1a8b18995eede6b0abb93964d58cf11bcea362f", + "sha256:730385fdb99a21fce9bb84bb7fcbda72c88626facd74956bda712834b480729d", + "sha256:7ccd5bd222e5041069ad9d9868ab59e6dbc53ecde8d8c82b919954fbba43b46b", + "sha256:7e8e4a571d958910272af8d53a9cbe6599f9f5fd496a1bc51211183bb2072cbd", + "sha256:811ac076855e33e931549340288e0761873baf29276ad00f221709933c644330", + "sha256:828c502bb261588f7de897e06cb23c4b122997cb039d2014cb78e7dabe92ef0c", + "sha256:838b898e8c1f26eb6b8d81b180981273f6f5110c76c22c384979aca854194f1b", + "sha256:860d0f5b42d0c0afd73fa4177709f6e1b966ba691fcd72175affa902052a81d6", + "sha256:8a730bf07feacb0863974e67b206b7c503a62199de1cece2eb0d4c233ec29c11", + "sha256:9156b96afa38db71344522f5517077eaedf62fcd2c9148392ff93d801128809c", + "sha256:9171e8e1a1f221953e38e84ae0abffe8759002fd8968106ee379febbb5358b33", + "sha256:978117122ca4cc59b28af5322253017f6c5fc03dbdda78c7f4b94ae984c8dd43", + "sha256:9b1b5adc5adf596c59dca57156b71ad301d73956f5bab4039b0e34dbf50b9fa0", + "sha256:9bcf56efdb83244cde070e82a69c0f03c47c235f0a5cb6c81d9da23af7fbaae4", + "sha256:a8c83718346de08d68b3cb1105c5d91e5fc39885d8610fdda16613d4e3941459", + "sha256:ae77275a28667d9c82d4522b681504642055efa0368d73108511647c6499b31c", + "sha256:b57c0954a9fdd2b05b9cec0f5a12a0bdce5bf021a5b3b09323041613972481ab", + "sha256:b812417199eeb169c25f67815cfb66fd8de7ff098bf57d065e8c1943a7ba5c8f", + "sha256:cfad553a36548262e7da0f3a7464270e13900b898800fb571a5d4b298c3f8356", + "sha256:d3222db9df629ef3c3673124f2e05fb72bc4a320c117e953fec0d69dde82e36d", + "sha256:d714595d81efab11b42bccd119977d94b25d12d3a806851ff6bfd286a4bce960", + "sha256:d92a3e835a5100f1d5b566fff79217eab92223ca31900dba733902a182a35ab0", + "sha256:ddc089315d030c54f0f03fb38286e2667c05009a78d659f108a8efcfbdf2e585", + "sha256:e3b0c4da61f39899561e08e571f54472a09fa71717d9797928af558175ae5243", + "sha256:eaaf80957c38e9d3f796f355a80fad945e72cd745e6b64c210e635b7043b673e", + "sha256:fa6b67f8bef277c2a4aadd548d58796854e7d760964126c3209b19bccc6a74f1", + "sha256:fc6bc65b0cf524ee042e0bc2912b9206ef242edfba7426cf95763e4af01f527a" ], "markers": "python_version >= '3.8'", - "version": "==3.9.12" + "version": "==3.9.13" }, "packaging": { "hashes": [ @@ -1232,25 +1265,25 @@ }, "polars": { "hashes": [ - "sha256:59845bae0b614b3291baa889cfc2a251e1024129696bb655596f2b5556e9f9a1", - "sha256:7c7b494beea914a54bcae8868dee3988a88ecb48525df948e07aacf2fb83e711", - "sha256:9e86736f68440bf97a9100fa0a79ae7ce616d1af6fd4669fff1345f03aab14c0", - "sha256:a96b157d68697c8d6ef2f7c2cc1734d498c3c6cc0c9c18d4fff7283ccfabdd1d", - "sha256:b53553308bc7e2b4f841b18f1949b61ed7f2cf155c5c64712298efa5af67a997", - "sha256:e4f4e3335fdcc863f6aac0616510b1baa5e13d5e818ebbfcb980ad534bd6edc2" + "sha256:359d556fafcb533bb0caa34ddfbd5161ee23b8a43817c7a2b80189720a1f42f6", + "sha256:3cfc71d4569818548c9bf4285c497d62a3a542363c86940593a41dd731e69d7f", + "sha256:ddff2fa419f15aa64ee23a94655fcb24b3e1b5c3eb30d124e3315deca2039a92", + "sha256:ec742fdf41e16ff699c043259ba94a11bbc2f7dcb978d768495db1ff2b3c5c20", + "sha256:f0928576a52eca47e14a8b98f4da22025b4b2fa32549f80f4d92c5187fd3f461", + "sha256:fd6100df0ca53614c3fa7136251e030fb70dee8833023edf7a3ac380f8e2dce5" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.20.6" + "version": "==0.20.7" }, "prefect": { "hashes": [ - "sha256:1b06b9c363985e346506a0db1b53f4cf5e5bcff2fb6bb3ffb32bb75fcfc6b4af", - "sha256:b287230bbd38bad831848e7f5da46a3116823d6131c1a6fbd0528ea3b9e29dc1" + "sha256:3a028800ada5349d756082886f9564ddfb218008380ee657eb89d3d6e8bb974b", + "sha256:bc74fe873f6565a69e8bb7a6f09025a9650d8851f371564759f542095253f445" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.14.20" + "version": "==2.14.21" }, "prefect-aws": { "hashes": [ @@ -1270,6 +1303,15 @@ "markers": "python_version >= '3.7'", "version": "==0.2.2" }, + "prefect-sqlalchemy": { + "hashes": [ + "sha256:113d6a6d179f8673ba4a0b5e42e0c22a86b8c773110061d01335ff9139208ea4", + "sha256:8e520e697c0d5d96be0bf3a0e72b40760e89e5c8ade0555a7c473ef17793ea47" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==0.4.0" + }, "prefecto": { "hashes": [ "sha256:ca0479267af291ccb612ead4b39ed61457dbf4965cf4a5b750d36a090eb5c7d9", @@ -1307,96 +1349,105 @@ "email" ], "hashes": [ - "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae", - "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf" + "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f", + "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9" ], "markers": "python_version >= '3.8'", - "version": "==2.6.0" + "version": "==2.6.1" }, "pydantic-core": { "hashes": [ - "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7", - "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca", - "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51", - "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da", - "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc", - "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae", - "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4", - "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b", - "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0", - "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e", - "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118", - "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506", - "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798", - "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f", - "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d", - "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948", - "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f", - "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9", - "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137", - "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640", - "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f", - "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff", - "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706", - "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d", - "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f", - "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c", - "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8", - "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1", - "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7", - "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95", - "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60", - "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253", - "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e", - "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c", - "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc", - "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3", - "sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8", - "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9", - "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c", - "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388", - "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95", - "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91", - "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818", - "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8", - "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f", - "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394", - "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13", - "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17", - "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7", - "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06", - "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f", - "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196", - "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66", - "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf", - "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c", - "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76", - "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0", - "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212", - "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f", - "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49", - "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206", - "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48", - "sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c", - "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2", - "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05", - "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610", - "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd", - "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76", - "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1", - "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60", - "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34", - "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4", - "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864", - "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66", - "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c", - "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e", - "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54", - "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8", - "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e" + "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379", + "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06", + "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05", + "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7", + "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753", + "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a", + "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731", + "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc", + "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380", + "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3", + "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c", + "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11", + "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990", + "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a", + "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2", + "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8", + "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97", + "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a", + "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8", + "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef", + "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77", + "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33", + "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82", + "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5", + "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b", + "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55", + "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e", + "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b", + "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7", + "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec", + "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc", + "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469", + "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b", + "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20", + "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e", + "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d", + "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f", + "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b", + "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039", + "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e", + "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2", + "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f", + "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b", + "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc", + "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8", + "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522", + "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e", + "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784", + "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a", + "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890", + "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485", + "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545", + "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f", + "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943", + "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878", + "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f", + "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17", + "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7", + "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286", + "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c", + "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb", + "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646", + "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978", + "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8", + "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15", + "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272", + "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2", + "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55", + "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf", + "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545", + "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4", + "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a", + "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804", + "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4", + "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0", + "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a", + "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113", + "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d", + "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25" ], "markers": "python_version >= '3.8'", - "version": "==2.16.1" + "version": "==2.16.2" + }, + "pydantic-settings": { + "hashes": [ + "sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c", + "sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.1.0" }, "pygments": { "hashes": [ @@ -1414,20 +1465,28 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, + "python-dotenv": { + "hashes": [ + "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", + "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" + ], + "markers": "python_version >= '3.8'", + "version": "==1.0.1" + }, "python-slugify": { "hashes": [ - "sha256:c71189c161e8c671f1b141034d9a56308a8a5978cd13d40446c879569212fdd1", - "sha256:e04cba5f1c562502a1175c84a8bc23890c54cdaf23fccaaf0bf78511508cabed" + "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", + "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856" ], "markers": "python_version >= '3.7'", - "version": "==8.0.3" + "version": "==8.0.4" }, "pytz": { "hashes": [ - "sha256:31d4583c4ed539cd037956140d695e42c033a19e984bfce9964a3f7d59bc2b40", - "sha256:f90ef520d95e7c46951105338d918664ebfd6f1d995bd7d153127ce90efafa6a" + "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", + "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" ], - "version": "==2023.4" + "version": "==2024.1" }, "pytzdata": { "hashes": [ @@ -1748,11 +1807,11 @@ }, "ruamel.yaml": { "hashes": [ - "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e", - "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada" + "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636", + "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b" ], "markers": "python_version >= '3.7'", - "version": "==0.18.5" + "version": "==0.18.6" }, "ruamel.yaml.clib": { "hashes": [ @@ -1863,58 +1922,67 @@ "asyncio" ], "hashes": [ - "sha256:0d3cab3076af2e4aa5693f89622bef7fa770c6fec967143e4da7508b3dceb9b9", - "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d", - "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e", - "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669", - "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d", - "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5", - "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002", - "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e", - "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd", - "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215", - "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24", - "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39", - "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62", - "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900", - "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf", - "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735", - "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4", - "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23", - "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9", - "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c", - "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de", - "sha256:798f717ae7c806d67145f6ae94dc7c342d3222d3b9a311a784f371a4333212c7", - "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625", - "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f", - "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a", - "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4", - "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643", - "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018", - "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9", - "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5", - "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08", - "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3", - "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3", - "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed", - "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95", - "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6", - "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0", - "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84", - "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f", - "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d", - "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2", - "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5", - "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570", - "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7", - "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c", - "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf", - "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3", - "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed", - "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5" + "sha256:02a4f954ccb17bd8cff56662efc806c5301508233dc38d0253a5fdb2f33ca3ba", + "sha256:06ed4d6bb2365222fb9b0a05478a2d23ad8c1dd874047a9ae1ca1d45f18a255e", + "sha256:1128b2cdf49107659f6d1f452695f43a20694cc9305a86e97b70793a1c74eeb4", + "sha256:1a870e6121a052f826f7ae1e4f0b54ca4c0ccd613278218ca036fa5e0f3be7df", + "sha256:379af901ceb524cbee5e15c1713bf9fd71dc28053286b7917525d01b938b9628", + "sha256:3f06afe8e96d7f221cc0b59334dc400151be22f432785e895e37030579d253c3", + "sha256:3fc557f5402206c18ec3d288422f8e5fa764306d49f4efbc6090a7407bf54938", + "sha256:4b4d848b095173e0a9e377127b814490499e55f5168f617ae2c07653c326b9d1", + "sha256:4f57af0866f6629eae2d24d022ba1a4c1bac9b16d45027bbfcda4c9d5b0d8f26", + "sha256:4fc0628d2026926404dabc903dc5628f7d936a792aa3a1fc54a20182df8e2172", + "sha256:5310958d08b4bafc311052be42a3b7d61a93a2bf126ddde07b85f712e7e4ac7b", + "sha256:56524d767713054f8758217b3a811f6a736e0ae34e7afc33b594926589aa9609", + "sha256:5901eed6d0e23ca4b04d66a561799d4f0fe55fcbfc7ca203bb8c3277f442085b", + "sha256:651d10fdba7984bf100222d6e4acc496fec46493262b6170be1981ef860c6184", + "sha256:691d68a4fca30c9a676623d094b600797699530e175b6524a9f57e3273f5fa8d", + "sha256:6d68e6b507a3dd20c0add86ac0a0ca061d43c9a0162a122baa5fe952f14240f1", + "sha256:6e25f029e8ad6d893538b5abe8537e7f09e21d8e96caee46a7e2199f3ddd77b0", + "sha256:750d1ef39d50520527c45c309c3cb10bbfa6131f93081b4e93858abb5ece2501", + "sha256:79a74a4ca4310c812f97bf0f13ce00ed73c890954b5a20b32484a9ab60e567e9", + "sha256:79e629df3f69f849a1482a2d063596b23e32036b83547397e68725e6e0d0a9ab", + "sha256:84d377645913d47f0dc802b415bcfe7fb085d86646a12278d77c12eb75b5e1b4", + "sha256:872f2907ade52601a1e729e85d16913c24dc1f6e7c57d11739f18dcfafde29db", + "sha256:8b39462c9588d4780f041e1b84d2ba038ac01c441c961bbee622dd8f53dec69f", + "sha256:8cbeb0e49b605cd75f825fb9239a554803ef2bef1a7b2a8b428926ed518b6b63", + "sha256:8f95ede696ab0d7328862d69f29b643d35b668c4f3619cb2f0281adc16e64c1b", + "sha256:94a78f56ea13f4d6e9efcd2a2d08cc13531918e0516563f6303c4ad98c81e21d", + "sha256:98f4d0d2bda2921af5b0c2ca99207cdab00f2922da46a6336c62c8d6814303a7", + "sha256:996b41c38e34a980e9f810d6e2709a3196e29ee34e46e3c16f96c63da10a9da1", + "sha256:99a9a8204b8937aa72421e31c493bfc12fd063a8310a0522e5a9b98e6323977c", + "sha256:a481cc2eec83776ff7b6bb12c8e85d0378af0e2ec4584ac3309365a2a380c64b", + "sha256:a678f728fb075e74aaa7fdc27f8af8f03f82d02e7419362cc8c2a605c16a4114", + "sha256:a9846ffee3283cff4ec476e7ee289314290fcb2384aab5045c6f481c5c4d011f", + "sha256:b39503c3a56e1b2340a7d09e185ddb60b253ad0210877a9958ac64208eb23674", + "sha256:b7ee16afd083bb6bb5ab3962ac7f0eafd1d196c6399388af35fef3d1c6d6d9bb", + "sha256:ba46fa770578b3cf3b5b77dadb7e94fda7692dd4d1989268ef3dcb65f31c40a3", + "sha256:c2d8a2c68b279617f13088bdc0fc0e9b5126f8017f8882ff08ee41909fab0713", + "sha256:caa79a6caeb4a3cc4ddb9aba9205c383f5d3bcb60d814e87e74570514754e073", + "sha256:d25fe55aab9b20ae4a9523bb269074202be9d92a145fcc0b752fff409754b5f6", + "sha256:dc32ecf643c4904dd413e6a95a3f2c8a89ccd6f15083e586dcf8f42eb4e317ae", + "sha256:e1a532bc33163fb19c4759a36504a23e63032bc8d47cee1c66b0b70a04a0957b", + "sha256:e1bcd8fcb30305e27355d553608c2c229d3e589fb7ff406da7d7e5d50fa14d0d", + "sha256:e70cce65239089390c193a7b0d171ce89d2e3dedf797f8010031b2aa2b1e9c80", + "sha256:ec3717c1efee8ad4b97f6211978351de3abe1e4b5f73e32f775c7becec021c5c", + "sha256:ed4667d3d5d6e203a271d684d5b213ebcd618f7a8bc605752a8865eb9e67a79a", + "sha256:f2efbbeb18c0e1c53b670a46a009fbde7b58e05b397a808c7e598532b17c6f4b", + "sha256:f75ac12d302205e60f77f46bd162d40dc37438f1f8db160d2491a78b19a0bd61", + "sha256:fab1bb909bd24accf2024a69edd4f885ded182c079c4dbcd515b4842f86b07cb", + "sha256:fb97a9b93b953084692a52a7877957b7a88dfcedc0c5652124f5aebf5999f7fe", + "sha256:fd133afb7e6c59fad365ffa97fb06b1001f88e29e1de351bef3d2b1224e2f132" ], "markers": "python_version >= '3.7'", - "version": "==2.0.25" + "version": "==2.0.26" + }, + "sqlmodel": { + "hashes": [ + "sha256:0bff8fc94af86b44925aa813f56cf6aabdd7f156b73259f2f60692c6a64ac90e", + "sha256:accea3ff5d878e41ac439b11e78613ed61ce300cfcb860e87a2d73d4884cbee4" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==0.0.14" }, "starlette": { "hashes": [ @@ -1958,11 +2026,11 @@ }, "tqdm": { "hashes": [ - "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386", - "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7" + "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9", + "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531" ], "markers": "python_version >= '3.7'", - "version": "==4.66.1" + "version": "==4.66.2" }, "typer": { "hashes": [ @@ -2069,11 +2137,11 @@ }, "uvicorn": { "hashes": [ - "sha256:4b85ba02b8a20429b9b205d015cbeb788a12da527f731811b643fd739ef90d5f", - "sha256:54898fcd80c13ff1cd28bf77b04ec9dbd8ff60c5259b499b4b12bb0917f22907" + "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a", + "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4" ], "markers": "python_version >= '3.8'", - "version": "==0.27.0.post1" + "version": "==0.27.1" }, "webencodings": { "hashes": [ @@ -2424,19 +2492,19 @@ }, "boto3": { "hashes": [ - "sha256:33a8b6d9136fa7427160edb92d2e50f2035f04e9d63a2d1027349053e12626aa", - "sha256:b2f321e20966f021ec800b7f2c01287a3dd04fc5965acdfbaa9c505a24ca45d1" + "sha256:35bcbecf1b5d3620c93f0062d2994177f8bda25a9d2cba144d6462793c16065b", + "sha256:476896e70d36c9134d4125834280c597c17b54bff4902baf2e5fcde74f8acec8" ], "markers": "python_version >= '3.8'", - "version": "==1.34.34" + "version": "==1.34.39" }, "botocore": { "hashes": [ - "sha256:54093dc97372bb7683f5c61a279aa8240408abf3b2cc494ae82a9a90c1b784b5", - "sha256:cd060b0d88ebb2b893f1411c1db7f2ba66cc18e52dcc57ad029564ef5fec437b" + "sha256:9f00bd5e4698bcdd37ce6e224a896baf58d209678ed92834944b767de9061cc5", + "sha256:e175360445424b83b0e28ae20d301b99cf44ff2c9d5ab1d8670899bec05a9753" ], "markers": "python_version >= '3.8'", - "version": "==1.34.34" + "version": "==1.34.39" }, "cachetools": { "hashes": [ @@ -2642,12 +2710,12 @@ }, "commitizen": { "hashes": [ - "sha256:3c799dc0ffb2c99d021edd32de55ddce182635bf6129ac6abf223696ccbdd1a7", - "sha256:5345a118f2a0b2b8fcfc5fcfea7945f8c984803836a1735199d469e865be4efb" + "sha256:21e19143d209fd6b1feb6332430ad91cf5d37ba09990c74d1f941ef1e9087898", + "sha256:e0f0bb2eb19a6bcc540902d32e5e3b53c2f7636e888bb1894f83efb416af8762" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==3.14.0" + "version": "==3.14.1" }, "coolname": { "hashes": [ @@ -2782,11 +2850,11 @@ }, "fsspec": { "hashes": [ - "sha256:8548d39e8810b59c38014934f6b31e57f40c1b20f911f4cc2b85389c7e9bf0cb", - "sha256:d800d87f72189a745fa3d6b033b9dc4a34ad069f60ca60b943a63599f5501960" + "sha256:817f969556fa5916bc682e02ca2045f96ff7f586d45110fcb76022063ad2c7d8", + "sha256:b6ad1a679f760dda52b1168c859d01b7b80648ea6f7f7c7f5a8a91dc3f3ecb84" ], "markers": "python_version >= '3.8'", - "version": "==2023.12.2" + "version": "==2024.2.0" }, "ghp-import": { "hashes": [ @@ -2884,11 +2952,11 @@ }, "griffe": { "hashes": [ - "sha256:76c4439eaa2737af46ae003c331ab6ca79c5365b552f7b5aed263a3b4125735b", - "sha256:db1da6d1d8e08cbb20f1a7dee8c09da940540c2d4c1bfa26a9091cf6fc36a9ec" + "sha256:5b8c023f366fe273e762131fe4bfd141ea56c09b3cb825aa92d06a82681cfd93", + "sha256:66c48a62e2ce5784b6940e603300fcfb807b6f099b94e7f753f1841661fd5c7c" ], "markers": "python_version >= '3.8'", - "version": "==0.40.0" + "version": "==0.40.1" }, "h11": { "hashes": [ @@ -2943,11 +3011,11 @@ }, "identify": { "hashes": [ - "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d", - "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34" + "sha256:a4316013779e433d08b96e5eabb7f641e6c7942e4ab5d4c509ebd2e7a8994aed", + "sha256:ee17bc9d499899bc9eaec1ac7bf2dc9eedd480db9d88b96d123d3b64a9d34f5d" ], "markers": "python_version >= '3.8'", - "version": "==2.5.33" + "version": "==2.5.34" }, "idna": { "hashes": [ @@ -3165,12 +3233,12 @@ }, "mkdocs-material": { "hashes": [ - "sha256:5b24df36d8ac6cecd611241ce6f6423ccde3e1ad89f8360c3f76d5565fc2d82a", - "sha256:e115b90fccf5cd7f5d15b0c2f8e6246b21041628b8f590630e7fca66ed7fcf6c" + "sha256:635df543c01c25c412d6c22991872267723737d5a2f062490f33b2da1c013c6d", + "sha256:a5d62b73b3b74349e45472bfadc129c871dd2d4add68d84819580597b2f50d5d" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==9.5.6" + "version": "==9.5.9" }, "mkdocs-material-extensions": { "hashes": [ @@ -3226,59 +3294,59 @@ }, "orjson": { "hashes": [ - "sha256:03ea7ee7e992532c2f4a06edd7ee1553f0644790553a118e003e3c405add41fa", - "sha256:06e42e899dde61eb1851a9fad7f1a21b8e4be063438399b63c07839b57668f6c", - "sha256:09d60450cda3fa6c8ed17770c3a88473a16460cd0ff2ba74ef0df663b6fd3bb8", - "sha256:0fc156fba60d6b50743337ba09f052d8afc8b64595112996d22f5fce01ab57da", - "sha256:12756a108875526b76e505afe6d6ba34960ac6b8c5ec2f35faf73ef161e97e07", - "sha256:1bb8f657c39ecdb924d02e809f992c9aafeb1ad70127d53fb573a6a6ab59d549", - "sha256:2849f88a0a12b8d94579b67486cbd8f3a49e36a4cb3d3f0ab352c596078c730c", - "sha256:29bf08e2eadb2c480fdc2e2daae58f2f013dff5d3b506edd1e02963b9ce9f8a9", - "sha256:2dfaf71499d6fd4153f5c86eebb68e3ec1bf95851b030a4b55c7637a37bbdee4", - "sha256:3186b18754befa660b31c649a108a915493ea69b4fc33f624ed854ad3563ac65", - "sha256:410f24309fbbaa2fab776e3212a81b96a1ec6037259359a32ea79fbccfcf76aa", - "sha256:4a0cd56e8ee56b203abae7d482ac0d233dbfb436bb2e2d5cbcb539fe1200a312", - "sha256:54071b7398cd3f90e4bb61df46705ee96cb5e33e53fc0b2f47dbd9b000e238e1", - "sha256:5586a533998267458fad3a457d6f3cdbddbcce696c916599fa8e2a10a89b24d3", - "sha256:59feb148392d9155f3bfed0a2a3209268e000c2c3c834fb8fe1a6af9392efcbf", - "sha256:5c157e999e5694475a5515942aebeed6e43f7a1ed52267c1c93dcfde7d78d421", - "sha256:61563d5d3b0019804d782137a4f32c72dc44c84e7d078b89d2d2a1adbaa47b52", - "sha256:640e2b5d8e36b970202cfd0799d11a9a4ab46cf9212332cd642101ec952df7c8", - "sha256:6492ff5953011e1ba9ed1bf086835fd574bd0a3cbe252db8e15ed72a30479081", - "sha256:659a8d7279e46c97661839035a1a218b61957316bf0202674e944ac5cfe7ed83", - "sha256:67426651faa671b40443ea6f03065f9c8e22272b62fa23238b3efdacd301df31", - "sha256:6b4e2bed7d00753c438e83b613923afdd067564ff7ed696bfe3a7b073a236e07", - "sha256:890e7519c0c70296253660455f77e3a194554a3c45e42aa193cdebc76a02d82b", - "sha256:950951799967558c214cd6cceb7ceceed6f81d2c3c4135ee4a2c9c69f58aa225", - "sha256:96e44b21fe407b8ed48afbb3721f3c8c8ce17e345fbe232bd4651ace7317782d", - "sha256:975e72e81a249174840d5a8df977d067b0183ef1560a32998be340f7e195c730", - "sha256:99e8cd005b3926c3db9b63d264bd05e1bf4451787cc79a048f27f5190a9a0311", - "sha256:a2b6f5252c92bcab3b742ddb3ac195c0fa74bed4319acd74f5d54d79ef4715dc", - "sha256:a4ae815a172a1f073b05b9e04273e3b23e608a0858c4e76f606d2d75fcabde0c", - "sha256:a84a0c3d4841a42e2571b1c1ead20a83e2792644c5827a606c50fc8af7ca4bee", - "sha256:ab8add018a53665042a5ae68200f1ad14c7953fa12110d12d41166f111724656", - "sha256:af17fa87bccad0b7f6fd8ac8f9cbc9ee656b4552783b10b97a071337616db3e4", - "sha256:b0e9d73cdbdad76a53a48f563447e0e1ce34bcecef4614eb4b146383e6e7d8c9", - "sha256:b159baecfda51c840a619948c25817d37733a4d9877fea96590ef8606468b362", - "sha256:bc82a4db9934a78ade211cf2e07161e4f068a461c1796465d10069cb50b32a80", - "sha256:bd1b8ec63f0bf54a50b498eedeccdca23bd7b658f81c524d18e410c203189365", - "sha256:c95488e4aa1d078ff5776b58f66bd29d628fa59adcb2047f4efd3ecb2bd41a71", - "sha256:cbbf313c9fb9d4f6cf9c22ced4b6682230457741daeb3d7060c5d06c2e73884a", - "sha256:cbd0f3555205bf2a60f8812133f2452d498dbefa14423ba90fe89f32276f7abf", - "sha256:cd52dec9eddf4c8c74392f3fd52fa137b5f2e2bed1d9ae958d879de5f7d7cded", - "sha256:cfdaede0fa5b500314ec7b1249c7e30e871504a57004acd116be6acdda3b8ab3", - "sha256:d3cfb76600c5a1e6be91326b8f3b83035a370e727854a96d801c1ea08b708073", - "sha256:d664880d7f016efbae97c725b243b33c2cbb4851ddc77f683fd1eec4a7894146", - "sha256:d6ce2062c4af43b92b0221ed4f445632c6bf4213f8a7da5396a122931377acd9", - "sha256:da908d23a3b3243632b523344403b128722a5f45e278a8343c2bb67538dff0e4", - "sha256:daa438bd8024e03bcea2c5a92cd719a663a58e223fba967296b6ab9992259dbf", - "sha256:dde1bc7c035f2d03aa49dc8642d9c6c9b1a81f2470e02055e76ed8853cfae0c3", - "sha256:e773f251258dd82795fd5daeac081d00b97bacf1548e44e71245543374874bcf", - "sha256:ed398f9a9d5a1bf55b6e362ffc80ac846af2122d14a8243a1e6510a4eabcb71e", - "sha256:f4098c7674901402c86ba6045a551a2ee345f9f7ed54eeffc7d86d155c8427e5" + "sha256:031df1026c7ea8303332d78711f180231e3ae8b564271fb748a03926587c5546", + "sha256:0d3ba9d88e20765335260d7b25547d7c571eee2b698200f97afa7d8c7cd668fc", + "sha256:0d691c44604941945b00e0a13b19a7d9c1a19511abadf0080f373e98fdeb6b31", + "sha256:0fd9a2101d04e85086ea6198786a3f016e45475f800712e6833e14bf9ce2832f", + "sha256:16946d095212a3dec552572c5d9bca7afa40f3116ad49695a397be07d529f1fa", + "sha256:1ab9dbdec3f13f3ea6f937564ce21651844cfbf2725099f2f490426acf683c23", + "sha256:23f21faf072ed3b60b5954686f98157e073f6a8068eaa58dbde83e87212eda84", + "sha256:266e55c83f81248f63cc93d11c5e3a53df49a5d2598fa9e9db5f99837a802d5d", + "sha256:2cc03a35bfc71c8ebf96ce49b82c2a7be6af4b3cd3ac34166fdb42ac510bbfff", + "sha256:2f37f0cdd026ef777a4336e599d8194c8357fc14760c2a5ddcfdf1965d45504b", + "sha256:31372ba3a9fe8ad118e7d22fba46bbc18e89039e3bfa89db7bc8c18ee722dca8", + "sha256:31fb66b41fb2c4c817d9610f0bc7d31345728d7b5295ac78b63603407432a2b2", + "sha256:3869d65561f10071d3e7f35ae58fd377056f67d7aaed5222f318390c3ad30339", + "sha256:3deadd8dc0e9ff844b5b656fa30a48dbee1c3b332d8278302dd9637f6b09f627", + "sha256:43fd6036b16bb6742d03dae62f7bdf8214d06dea47e4353cde7e2bd1358d186f", + "sha256:446d9ad04204e79229ae19502daeea56479e55cbc32634655d886f5a39e91b44", + "sha256:4584e8eb727bc431baaf1bf97e35a1d8a0109c924ec847395673dfd5f4ef6d6f", + "sha256:49b7e3fe861cb246361825d1a238f2584ed8ea21e714bf6bb17cebb86772e61c", + "sha256:5b98cd948372f0eb219bc309dee4633db1278687161e3280d9e693b6076951d2", + "sha256:5ef58869f3399acbbe013518d8b374ee9558659eef14bca0984f67cb1fbd3c37", + "sha256:60da7316131185d0110a1848e9ad15311e6c8938ee0b5be8cbd7261e1d80ee8f", + "sha256:62e9a99879c4d5a04926ac2518a992134bfa00d546ea5a4cae4b9be454d35a22", + "sha256:63ef57a53bfc2091a7cd50a640d9ae866bd7d92a5225a1bab6baa60ef62583f2", + "sha256:6e47153db080f5e87e8ba638f1a8b18995eede6b0abb93964d58cf11bcea362f", + "sha256:730385fdb99a21fce9bb84bb7fcbda72c88626facd74956bda712834b480729d", + "sha256:7ccd5bd222e5041069ad9d9868ab59e6dbc53ecde8d8c82b919954fbba43b46b", + "sha256:7e8e4a571d958910272af8d53a9cbe6599f9f5fd496a1bc51211183bb2072cbd", + "sha256:811ac076855e33e931549340288e0761873baf29276ad00f221709933c644330", + "sha256:828c502bb261588f7de897e06cb23c4b122997cb039d2014cb78e7dabe92ef0c", + "sha256:838b898e8c1f26eb6b8d81b180981273f6f5110c76c22c384979aca854194f1b", + "sha256:860d0f5b42d0c0afd73fa4177709f6e1b966ba691fcd72175affa902052a81d6", + "sha256:8a730bf07feacb0863974e67b206b7c503a62199de1cece2eb0d4c233ec29c11", + "sha256:9156b96afa38db71344522f5517077eaedf62fcd2c9148392ff93d801128809c", + "sha256:9171e8e1a1f221953e38e84ae0abffe8759002fd8968106ee379febbb5358b33", + "sha256:978117122ca4cc59b28af5322253017f6c5fc03dbdda78c7f4b94ae984c8dd43", + "sha256:9b1b5adc5adf596c59dca57156b71ad301d73956f5bab4039b0e34dbf50b9fa0", + "sha256:9bcf56efdb83244cde070e82a69c0f03c47c235f0a5cb6c81d9da23af7fbaae4", + "sha256:a8c83718346de08d68b3cb1105c5d91e5fc39885d8610fdda16613d4e3941459", + "sha256:ae77275a28667d9c82d4522b681504642055efa0368d73108511647c6499b31c", + "sha256:b57c0954a9fdd2b05b9cec0f5a12a0bdce5bf021a5b3b09323041613972481ab", + "sha256:b812417199eeb169c25f67815cfb66fd8de7ff098bf57d065e8c1943a7ba5c8f", + "sha256:cfad553a36548262e7da0f3a7464270e13900b898800fb571a5d4b298c3f8356", + "sha256:d3222db9df629ef3c3673124f2e05fb72bc4a320c117e953fec0d69dde82e36d", + "sha256:d714595d81efab11b42bccd119977d94b25d12d3a806851ff6bfd286a4bce960", + "sha256:d92a3e835a5100f1d5b566fff79217eab92223ca31900dba733902a182a35ab0", + "sha256:ddc089315d030c54f0f03fb38286e2667c05009a78d659f108a8efcfbdf2e585", + "sha256:e3b0c4da61f39899561e08e571f54472a09fa71717d9797928af558175ae5243", + "sha256:eaaf80957c38e9d3f796f355a80fad945e72cd745e6b64c210e635b7043b673e", + "sha256:fa6b67f8bef277c2a4aadd548d58796854e7d760964126c3209b19bccc6a74f1", + "sha256:fc6bc65b0cf524ee042e0bc2912b9206ef242edfba7426cf95763e4af01f527a" ], "markers": "python_version >= '3.8'", - "version": "==3.9.12" + "version": "==3.9.13" }, "packaging": { "hashes": [ @@ -3331,12 +3399,12 @@ }, "pipenv": { "hashes": [ - "sha256:067b1c94a7807f424f63660be8b4c1886b6b9db99bd80d223794da273cc4589c", - "sha256:c3153c23d19384efe8a51eea4852549ceac1904093f6acdb2666fe317bb6a70c" + "sha256:4aea73e23944e464ad2b849328e780ad121c5336e1c24a7ac15aa493c41c2341", + "sha256:96c8af7c36691fbc648959f3f631954212398246c8cfcfa529ec09bc5d0bfd01" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2023.12.0" + "version": "==2023.12.1" }, "platformdirs": { "hashes": [ @@ -3356,21 +3424,21 @@ }, "pre-commit": { "hashes": [ - "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376", - "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d" + "sha256:9fe989afcf095d2c4796ce7c553cf28d4d4a9b9346de3cda079bcf40748454a4", + "sha256:c90961d8aa706f75d60935aba09469a6b0bcb8345f127c3fbee4bdc5f114cf4b" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.6.0" + "version": "==3.6.1" }, "prefect": { "hashes": [ - "sha256:1b06b9c363985e346506a0db1b53f4cf5e5bcff2fb6bb3ffb32bb75fcfc6b4af", - "sha256:b287230bbd38bad831848e7f5da46a3116823d6131c1a6fbd0528ea3b9e29dc1" + "sha256:3a028800ada5349d756082886f9564ddfb218008380ee657eb89d3d6e8bb974b", + "sha256:bc74fe873f6565a69e8bb7a6f09025a9650d8851f371564759f542095253f445" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.14.20" + "version": "==2.14.21" }, "prefect-github": { "hashes": [ @@ -3441,96 +3509,96 @@ "email" ], "hashes": [ - "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae", - "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf" + "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f", + "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9" ], "markers": "python_version >= '3.8'", - "version": "==2.6.0" + "version": "==2.6.1" }, "pydantic-core": { "hashes": [ - "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7", - "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca", - "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51", - "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da", - "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc", - "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae", - "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4", - "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b", - "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0", - "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e", - "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118", - "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506", - "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798", - "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f", - "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d", - "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948", - "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f", - "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9", - "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137", - "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640", - "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f", - "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff", - "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706", - "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d", - "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f", - "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c", - "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8", - "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1", - "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7", - "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95", - "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60", - "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253", - "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e", - "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c", - "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc", - "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3", - "sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8", - "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9", - "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c", - "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388", - "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95", - "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91", - "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818", - "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8", - "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f", - "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394", - "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13", - "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17", - "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7", - "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06", - "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f", - "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196", - "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66", - "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf", - "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c", - "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76", - "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0", - "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212", - "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f", - "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49", - "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206", - "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48", - "sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c", - "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2", - "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05", - "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610", - "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd", - "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76", - "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1", - "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60", - "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34", - "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4", - "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864", - "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66", - "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c", - "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e", - "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54", - "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8", - "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e" + "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379", + "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06", + "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05", + "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7", + "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753", + "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a", + "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731", + "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc", + "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380", + "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3", + "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c", + "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11", + "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990", + "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a", + "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2", + "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8", + "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97", + "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a", + "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8", + "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef", + "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77", + "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33", + "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82", + "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5", + "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b", + "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55", + "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e", + "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b", + "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7", + "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec", + "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc", + "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469", + "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b", + "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20", + "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e", + "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d", + "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f", + "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b", + "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039", + "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e", + "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2", + "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f", + "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b", + "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc", + "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8", + "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522", + "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e", + "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784", + "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a", + "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890", + "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485", + "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545", + "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f", + "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943", + "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878", + "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f", + "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17", + "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7", + "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286", + "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c", + "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb", + "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646", + "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978", + "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8", + "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15", + "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272", + "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2", + "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55", + "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf", + "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545", + "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4", + "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a", + "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804", + "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4", + "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0", + "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a", + "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113", + "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d", + "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25" ], "markers": "python_version >= '3.8'", - "version": "==2.16.1" + "version": "==2.16.2" }, "pyflakes": { "hashes": [ @@ -3542,35 +3610,35 @@ }, "pygit2": { "hashes": [ - "sha256:0d7526a7ad2bb91b36ba43c87452182052f58cb068311cf8173ed5391ca7788e", - "sha256:0f101c08fe2f81cc05a44f5c95ea5396310df3240e24d9f5dc2cf1871a794fcb", - "sha256:12a5f456ab9ac2e7718c95c8ac2bfa1fd23908545deb7cb7693e035c2d0f037a", - "sha256:34a05d47b05e1fe2cc44164d778035253868b179819b300a4d1c6cb75ff48847", - "sha256:3adf7fd8af9bc3b6e11e4920abb0121cdad6f8299ed1d7643e756ab49dbb4e34", - "sha256:47d8223440096e59bd6367c341692cd0191e98601665dd4986ba2e00bc5ef769", - "sha256:4c74aba5b40d6dac2f04bf4f3ca529304bdbf77888de0e87c706d243c9fa0693", - "sha256:613bc82b0a17ccd5334b8f5d3b963698b45e228910bcea27fa52f84c60f50b1a", - "sha256:65cc2e696f5d6add54d34dbf7336a420f7b1df31c525e3ed5c8a123f4f1d67de", - "sha256:7ec66cb115afd5552d50ba96a29e60da4556cd060396a1b38e97aefc047bd124", - "sha256:807cf57e02947ad448ae91226d193ebe0999540a56f5a95183a502e28c50b7ff", - "sha256:80d0baca5ab9a06ca6a709716737ed6993e10349db7a98f1f3966278d39098fd", - "sha256:84dd4b36e38c9736736ba57e7257b6efe604932232c98503a64c94283dada7de", - "sha256:86f5295e7996927238dfebdb3c8d81dae83332bc8ced61971806a606261d60ff", - "sha256:87ea6fd663ebe59e6e872a25a0f1af2d83c7d75147461a352a22bca4df70c8d0", - "sha256:a83fe40e2cdac3abf926b633e07be434ddae353085720c1a6e3afb2a4b72f9c1", - "sha256:a98c3db4f06bae8266263bdc7b7447801debc30b6223f0826e07709abe9c0929", - "sha256:ab5a983cb116d617c136cdc23832e16aed17f5fdd3b7bb46d85c0aabde0162ee", - "sha256:b0384fb21af58149d59dc37f73f9daea7e6cfec2de7d067be40cc08049b4a62b", - "sha256:bb10402c983d8513c3bceb6a3f6f52ec19c69b0244801cebe95aab6dbf19f679", - "sha256:e352b77c2e6f8a1900b406bc10a9471718782775a6029d847c71e5363c3166f9", - "sha256:ed9e67e58f11f285e2fa2077c6f45852763826f8b8a2a777937f1fd2313eed5d", - "sha256:f529ed9660edbf9b625ccae7e51098ef73662e61496609009772d4627a826aa8", - "sha256:fb53c367f66cdd8d41552ed2a01a15a0499d8373dcca37360f3abfb7bf947f71", - "sha256:ffe8b5b7fb482c3f8625384eb60e83390e1c2c1b74e66aff2f812e74c9754c5d" + "sha256:0fff3d1aaf1d7372757888c4620347d6ad8b1b3a637b30a3abd156da7cf9476b", + "sha256:11058be23a5d6c1308303fd450d690eada117c564154634d81676e66530056be", + "sha256:141a1b37fc431d98b3de2f4651eab8b1b1b038cd50de42bfd1c8de057ec2284e", + "sha256:15db91695259f672f8be3080eb943889f7c8bdc5fbd8b89555e0c53ba2481f15", + "sha256:230493d43945e10365070d349da206d39cc885ae8c52fdeca93942f36661dd93", + "sha256:404d3d9bac22ff022157de3fbfd8997c108d86814ba88cbc8709c1c2daef833a", + "sha256:46ae2149851d5da2934e27c9ac45c375d04af1e549f8c4cbb4e9e4de5f43dc42", + "sha256:67b6e5911101dc5ecb679bf241c0b9ee2099f4d76aa0ad66b326400cb4590afa", + "sha256:760614370fcce4e9606ff675d6fc11165badb59aaedc2ea6cb2e7ec1855616c2", + "sha256:793f49ce66640d41d977e1337ddb5dec9b3b4ff818040d78d3ded052e1ea52e6", + "sha256:7b6d1202d6a0c21281d2697321292aff9e2e2e195d6ce553efcdf86c2de2af1a", + "sha256:8589c8c0005b5ba373b3b101f903d4451338f3dfc09f8a38c76da6584fef84d0", + "sha256:9d96e46b94dc706e6316e6cc293c0a0692e5b0811a6f8f2738728a4a68d7a827", + "sha256:a03de11ba5205628996d867280e5181605009c966c801dbb94781bed55b740d7", + "sha256:acb849cea89438192e78eea91a27fb9c54c7286a82aac65a3f746ea8c498fedb", + "sha256:acc7be8a439274fc6227e33b63b9ec83cd51fa210ab898eaadffb7bf930c0087", + "sha256:bc3326a5ce891ef26429ae6d4290acb80ea0064947b4184a4c4940b4bd6ab4a3", + "sha256:c22027f748d125698964ed696406075dac85f114e01d50547e67053c1bb03308", + "sha256:e4f371c4b7ee86c0a751209fac7c941d1f6a3aca6af89ac09481469dbe0ea1cc", + "sha256:ea505739af41496b1d36c99bc15e2bd5631880059514458977c8931e27063a8d", + "sha256:ec5958571b82a6351785ca645e5394c31ae45eec5384b2fa9c4e05dde3597ad6", + "sha256:ed16f2bc8ca9c42af8adb967c73227b1de973e9c4d717bd738fb2f177890ca2c", + "sha256:f2378f9a70cea27809a2c78b823e22659691a91db9d81b1f3a58d537067815ac", + "sha256:f35152b96a31ab705cdd63aef08fb199d6c1e87fc6fd45b1945f8cd040a43b7b", + "sha256:f5a87744e6c36f03fe488b975c73d3eaef22eadce433152516a2b8dbc4015233" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.14.0" + "version": "==1.14.1" }, "pygments": { "hashes": [ @@ -3610,24 +3678,23 @@ "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" ], - "index": "pypi", "markers": "python_version >= '3.8'", "version": "==1.0.1" }, "python-slugify": { "hashes": [ - "sha256:c71189c161e8c671f1b141034d9a56308a8a5978cd13d40446c879569212fdd1", - "sha256:e04cba5f1c562502a1175c84a8bc23890c54cdaf23fccaaf0bf78511508cabed" + "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", + "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856" ], "markers": "python_version >= '3.7'", - "version": "==8.0.3" + "version": "==8.0.4" }, "pytz": { "hashes": [ - "sha256:31d4583c4ed539cd037956140d695e42c033a19e984bfce9964a3f7d59bc2b40", - "sha256:f90ef520d95e7c46951105338d918664ebfd6f1d995bd7d153127ce90efafa6a" + "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", + "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" ], - "version": "==2023.4" + "version": "==2024.1" }, "pytzdata": { "hashes": [ @@ -3972,11 +4039,11 @@ }, "ruamel.yaml": { "hashes": [ - "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e", - "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada" + "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636", + "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b" ], "markers": "python_version >= '3.7'", - "version": "==0.18.5" + "version": "==0.18.6" }, "ruamel.yaml.clib": { "hashes": [ @@ -4079,58 +4146,58 @@ "asyncio" ], "hashes": [ - "sha256:0d3cab3076af2e4aa5693f89622bef7fa770c6fec967143e4da7508b3dceb9b9", - "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d", - "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e", - "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669", - "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d", - "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5", - "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002", - "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e", - "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd", - "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215", - "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24", - "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39", - "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62", - "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900", - "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf", - "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735", - "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4", - "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23", - "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9", - "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c", - "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de", - "sha256:798f717ae7c806d67145f6ae94dc7c342d3222d3b9a311a784f371a4333212c7", - "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625", - "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f", - "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a", - "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4", - "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643", - "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018", - "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9", - "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5", - "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08", - "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3", - "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3", - "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed", - "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95", - "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6", - "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0", - "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84", - "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f", - "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d", - "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2", - "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5", - "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570", - "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7", - "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c", - "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf", - "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3", - "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed", - "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5" + "sha256:02a4f954ccb17bd8cff56662efc806c5301508233dc38d0253a5fdb2f33ca3ba", + "sha256:06ed4d6bb2365222fb9b0a05478a2d23ad8c1dd874047a9ae1ca1d45f18a255e", + "sha256:1128b2cdf49107659f6d1f452695f43a20694cc9305a86e97b70793a1c74eeb4", + "sha256:1a870e6121a052f826f7ae1e4f0b54ca4c0ccd613278218ca036fa5e0f3be7df", + "sha256:379af901ceb524cbee5e15c1713bf9fd71dc28053286b7917525d01b938b9628", + "sha256:3f06afe8e96d7f221cc0b59334dc400151be22f432785e895e37030579d253c3", + "sha256:3fc557f5402206c18ec3d288422f8e5fa764306d49f4efbc6090a7407bf54938", + "sha256:4b4d848b095173e0a9e377127b814490499e55f5168f617ae2c07653c326b9d1", + "sha256:4f57af0866f6629eae2d24d022ba1a4c1bac9b16d45027bbfcda4c9d5b0d8f26", + "sha256:4fc0628d2026926404dabc903dc5628f7d936a792aa3a1fc54a20182df8e2172", + "sha256:5310958d08b4bafc311052be42a3b7d61a93a2bf126ddde07b85f712e7e4ac7b", + "sha256:56524d767713054f8758217b3a811f6a736e0ae34e7afc33b594926589aa9609", + "sha256:5901eed6d0e23ca4b04d66a561799d4f0fe55fcbfc7ca203bb8c3277f442085b", + "sha256:651d10fdba7984bf100222d6e4acc496fec46493262b6170be1981ef860c6184", + "sha256:691d68a4fca30c9a676623d094b600797699530e175b6524a9f57e3273f5fa8d", + "sha256:6d68e6b507a3dd20c0add86ac0a0ca061d43c9a0162a122baa5fe952f14240f1", + "sha256:6e25f029e8ad6d893538b5abe8537e7f09e21d8e96caee46a7e2199f3ddd77b0", + "sha256:750d1ef39d50520527c45c309c3cb10bbfa6131f93081b4e93858abb5ece2501", + "sha256:79a74a4ca4310c812f97bf0f13ce00ed73c890954b5a20b32484a9ab60e567e9", + "sha256:79e629df3f69f849a1482a2d063596b23e32036b83547397e68725e6e0d0a9ab", + "sha256:84d377645913d47f0dc802b415bcfe7fb085d86646a12278d77c12eb75b5e1b4", + "sha256:872f2907ade52601a1e729e85d16913c24dc1f6e7c57d11739f18dcfafde29db", + "sha256:8b39462c9588d4780f041e1b84d2ba038ac01c441c961bbee622dd8f53dec69f", + "sha256:8cbeb0e49b605cd75f825fb9239a554803ef2bef1a7b2a8b428926ed518b6b63", + "sha256:8f95ede696ab0d7328862d69f29b643d35b668c4f3619cb2f0281adc16e64c1b", + "sha256:94a78f56ea13f4d6e9efcd2a2d08cc13531918e0516563f6303c4ad98c81e21d", + "sha256:98f4d0d2bda2921af5b0c2ca99207cdab00f2922da46a6336c62c8d6814303a7", + "sha256:996b41c38e34a980e9f810d6e2709a3196e29ee34e46e3c16f96c63da10a9da1", + "sha256:99a9a8204b8937aa72421e31c493bfc12fd063a8310a0522e5a9b98e6323977c", + "sha256:a481cc2eec83776ff7b6bb12c8e85d0378af0e2ec4584ac3309365a2a380c64b", + "sha256:a678f728fb075e74aaa7fdc27f8af8f03f82d02e7419362cc8c2a605c16a4114", + "sha256:a9846ffee3283cff4ec476e7ee289314290fcb2384aab5045c6f481c5c4d011f", + "sha256:b39503c3a56e1b2340a7d09e185ddb60b253ad0210877a9958ac64208eb23674", + "sha256:b7ee16afd083bb6bb5ab3962ac7f0eafd1d196c6399388af35fef3d1c6d6d9bb", + "sha256:ba46fa770578b3cf3b5b77dadb7e94fda7692dd4d1989268ef3dcb65f31c40a3", + "sha256:c2d8a2c68b279617f13088bdc0fc0e9b5126f8017f8882ff08ee41909fab0713", + "sha256:caa79a6caeb4a3cc4ddb9aba9205c383f5d3bcb60d814e87e74570514754e073", + "sha256:d25fe55aab9b20ae4a9523bb269074202be9d92a145fcc0b752fff409754b5f6", + "sha256:dc32ecf643c4904dd413e6a95a3f2c8a89ccd6f15083e586dcf8f42eb4e317ae", + "sha256:e1a532bc33163fb19c4759a36504a23e63032bc8d47cee1c66b0b70a04a0957b", + "sha256:e1bcd8fcb30305e27355d553608c2c229d3e589fb7ff406da7d7e5d50fa14d0d", + "sha256:e70cce65239089390c193a7b0d171ce89d2e3dedf797f8010031b2aa2b1e9c80", + "sha256:ec3717c1efee8ad4b97f6211978351de3abe1e4b5f73e32f775c7becec021c5c", + "sha256:ed4667d3d5d6e203a271d684d5b213ebcd618f7a8bc605752a8865eb9e67a79a", + "sha256:f2efbbeb18c0e1c53b670a46a009fbde7b58e05b397a808c7e598532b17c6f4b", + "sha256:f75ac12d302205e60f77f46bd162d40dc37438f1f8db160d2491a78b19a0bd61", + "sha256:fab1bb909bd24accf2024a69edd4f885ded182c079c4dbcd515b4842f86b07cb", + "sha256:fb97a9b93b953084692a52a7877957b7a88dfcedc0c5652124f5aebf5999f7fe", + "sha256:fd133afb7e6c59fad365ffa97fb06b1001f88e29e1de351bef3d2b1224e2f132" ], "markers": "python_version >= '3.7'", - "version": "==2.0.25" + "version": "==2.0.26" }, "starlette": { "hashes": [ @@ -4276,11 +4343,11 @@ }, "uvicorn": { "hashes": [ - "sha256:4b85ba02b8a20429b9b205d015cbeb788a12da527f731811b643fd739ef90d5f", - "sha256:54898fcd80c13ff1cd28bf77b04ec9dbd8ff60c5259b499b4b12bb0917f22907" + "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a", + "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4" ], "markers": "python_version >= '3.8'", - "version": "==0.27.0.post1" + "version": "==0.27.1" }, "virtualenv": { "hashes": [ @@ -4292,36 +4359,38 @@ }, "watchdog": { "hashes": [ - "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a", - "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100", - "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8", - "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc", - "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae", - "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41", - "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0", - "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f", - "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c", - "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9", - "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3", - "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709", - "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83", - "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759", - "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9", - "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3", - "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7", - "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f", - "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346", - "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674", - "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397", - "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96", - "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d", - "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a", - "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64", - "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44", - "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33" + "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257", + "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca", + "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b", + "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85", + "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b", + "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19", + "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50", + "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92", + "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269", + "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f", + "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c", + "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b", + "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87", + "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b", + "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b", + "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8", + "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c", + "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3", + "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7", + "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605", + "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935", + "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b", + "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927", + "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101", + "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07", + "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec", + "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4", + "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245", + "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d" ], - "markers": "python_version >= '3.7'", - "version": "==3.0.0" + "markers": "python_version >= '3.8'", + "version": "==4.0.0" }, "wcwidth": { "hashes": [ diff --git a/infrastructure/borderlands-aurora/CREATE_IAM_USER.sql b/infrastructure/borderlands-aurora/CREATE_IAM_USER.sql index aca22cb..99e182a 100644 --- a/infrastructure/borderlands-aurora/CREATE_IAM_USER.sql +++ b/infrastructure/borderlands-aurora/CREATE_IAM_USER.sql @@ -24,8 +24,9 @@ GRANT -- permissions for more complex routines CREATE TEMPORARY TABLES, EXECUTE, - TRIGGER -ON Borderlands.* TO 'BorderlandsExecutor'; + TRIGGER, + LOAD FROM S3 +ON borderlands.* TO 'BorderlandsExecutor'; -- Grants the RDS user the necessary permissions to read and write to the Borderlands database GRANT 'BorderlandsExecutor' TO 'Prefect'; diff --git a/infrastructure/terraform/aurora.tf b/infrastructure/terraform/aurora.tf index e8a602f..48b3152 100644 --- a/infrastructure/terraform/aurora.tf +++ b/infrastructure/terraform/aurora.tf @@ -26,14 +26,65 @@ resource "aws_iam_role" "rds" { } } +data "aws_iam_policy_document" "s3_read" { + statement { + effect = "Allow" + actions = [ + "s3:GetObject" + ] + resources = [ + "${aws_s3_bucket.core_bucket.arn}/*" + ] + } +} + +resource "aws_iam_policy" "rds_s3_read" { + name = "rds-s3-read" + description = "Allows RDS to read from S3" + policy = data.aws_iam_policy_document.s3_read.json +} + +resource "aws_iam_role_policy_attachment" "rds_s3_read" { + policy_arn = aws_iam_policy.rds_s3_read.arn + role = aws_iam_role.rds.name +} + // ------------------------------ // RDS Cluster +resource "aws_rds_cluster_parameter_group" "dev" { + name = "rds-cluster-pg-borderlands-dev" + family = "aurora-mysql8.0" + description = "Custom parameter group for the Borderlands dev RDS cluster" + + parameter { + name = "aws_default_s3_role" + value = aws_iam_role.rds.arn + } + + parameter { + // Allows Prefect DB user to automatically assume the BorderlandsExecutor role + name = "activate_all_roles_on_login" + value = "TRUE" + } + + tags = { + project = "borderlands" + } +} + resource "aws_rds_cluster" "borderlands_dev" { // Cluster config cluster_identifier = "borderlands-dev" engine = "aurora-mysql" + engine_mode = "provisioned" engine_version = "8.0.mysql_aurora.3.04.1" + db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.dev.name + + serverlessv2_scaling_configuration { + max_capacity = 1.0 + min_capacity = 0.5 + } // DB instance database_name = "borderlands" @@ -66,7 +117,7 @@ resource "aws_rds_cluster" "borderlands_dev" { resource "aws_rds_cluster_instance" "instance_1" { cluster_identifier = aws_rds_cluster.borderlands_dev.cluster_identifier identifier = "borderlands-dev-instance-1" - instance_class = "db.t3.medium" + instance_class = "db.serverless" engine = "aurora-mysql" engine_version = "8.0.mysql_aurora.3.04.1" @@ -103,6 +154,10 @@ resource "aws_iam_user_policy" "prefect_user_dev_rds_access" { policy = data.aws_iam_policy_document.dev_rds_access.json } + +// ------------------------------ +// RDS Cluster Outputs + output "rds_cluster_endpoint" { description = "The endpoint of the RDS cluster." value = aws_rds_cluster.borderlands_dev.endpoint diff --git a/src/borderlands/blocks.py b/src/borderlands/blocks.py index aab52be..2fa1c3c 100644 --- a/src/borderlands/blocks.py +++ b/src/borderlands/blocks.py @@ -3,103 +3,123 @@ """ import asyncio +from typing import Awaitable +from prefect.blocks.core import Block from prefect.utilities.asyncutils import sync_compatible from prefect_aws import S3Bucket from prefect_slack import SlackWebhook -from prefecto.filesystems import create_child +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict +from .utilities.blocks import RdsCredentials -class Blocks: + +class Blocks(BaseSettings): """Class for lazy loading Prefect Blocks.""" - _core_bucket: S3Bucket | None = None - _persistence_bucket: S3Bucket | None = None - _oryx_bucket: S3Bucket | None = None - _assets_bucket: S3Bucket | None = None - _media_bucket: S3Bucket | None = None - _webhook: SlackWebhook | None = None + model_config = SettingsConfigDict( + env_prefix="blocks_", + case_sensitive=False, + ) + + core_bucket_name: str = Field( + "s3-bucket-borderlands-core", + description="The name of the S3 bucket for result data.", + ) + + persistence_bucket_name: str = Field( + "s3-bucket-borderlands-persistence", + description="The name of the S3 bucket for Prefect persistence data.", + ) + + webhook_name: str = Field( + "slack-webhook-borderlands", + description="The name of the Slack webhook for notifications.", + ) + + rds_credentials_name: str = Field( + "rds-credentials-borderlands", + description="The name of the RDS credentials for the program.", + ) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._core_bucket: S3Bucket | None = None + self._persistence_bucket: S3Bucket | None = None + self._webhook: SlackWebhook | None = None + self._rds_credentials: RdsCredentials | None = None + + def copy(self) -> "Blocks": + """Return a copy of the blocks.""" + new_blocks = Blocks() + + new_blocks._core_bucket = self._core_bucket + new_blocks.core_bucket_name = self.core_bucket_name + + new_blocks._persistence_bucket = self._persistence_bucket + new_blocks.persistence_bucket_name = self.persistence_bucket_name + + new_blocks._webhook = self._webhook + new_blocks.webhook_name = self.webhook_name + + new_blocks._rds_credentials = self._rds_credentials + new_blocks.rds_credentials_name = self.rds_credentials_name + return new_blocks + + def reset(self): + """Reset the blocks.""" + self._core_bucket = None + self._persistence_bucket = None + self._webhook = None + self._rds_credentials = None @property def core_bucket(self) -> S3Bucket: """Returns the bucket for the program. Loads if it isn't already.""" if not self._core_bucket: - self._core_bucket = S3Bucket.load("s3-bucket-borderlands-core") + self._core_bucket = S3Bucket.load(self.core_bucket_name) return self._core_bucket @property def persistence_bucket(self) -> S3Bucket: """Returns the bucket for the program. Loads if it isn't already.""" if not self._persistence_bucket: - self._persistence_bucket = S3Bucket.load( - "s3-bucket-borderlands-persistence" - ) + self._persistence_bucket = S3Bucket.load(self.persistence_bucket_name) return self._persistence_bucket @property def webhook(self) -> SlackWebhook: """Returns the webhook for the program. Loads if it isn't already.""" if not self._webhook: - self._webhook = SlackWebhook.load("slack-webhook-borderlands") + self._webhook = SlackWebhook.load(self.webhook_name) return self._webhook @property - def oryx_bucket(self) -> S3Bucket: - """Returns the bucket for the program. Loads if it isn't already.""" - if not self._oryx_bucket: - self._oryx_bucket = create_child(self.core_bucket, "oryx", "-oryx") - return self._oryx_bucket - - @property - def assets_bucket(self) -> S3Bucket: - """Returns the bucket for the program. Loads if it isn't already.""" - if not self._assets_bucket: - self._assets_bucket = create_child(self.core_bucket, "assets", "-assets") - return self._assets_bucket - - @property - def media_bucket(self) -> S3Bucket: - """Returns the bucket for the program. Loads if it isn't already.""" - if not self._media_bucket: - self._media_bucket = create_child(self.core_bucket, "media", "-media") - return self._media_bucket + def rds_credentials(self) -> RdsCredentials: + """Returns the RDS credentials for the program. Loads if it isn't already.""" + if not self._rds_credentials: + self._rds_credentials = RdsCredentials.load(self.rds_credentials_name) + return self._rds_credentials @sync_compatible async def load(self): """Load the blocks.""" - self._core_bucket = ( - (await self.core_bucket) - if asyncio.iscoroutine(self.core_bucket) - else self.core_bucket - ) - - self._persistence_bucket = ( - (await self.persistence_bucket) - if asyncio.iscoroutine(self.persistence_bucket) - else self.persistence_bucket - ) - - self._webhook = ( - (await self.webhook) if asyncio.iscoroutine(self.webhook) else self.webhook - ) - - self._oryx_bucket = ( - (await self.oryx_bucket) - if asyncio.iscoroutine(self.oryx_bucket) - else self.oryx_bucket - ) - - self._assets_bucket = ( - (await self.assets_bucket) - if asyncio.iscoroutine(self.assets_bucket) - else self.assets_bucket - ) - - self._media_bucket = ( - (await self.media_bucket) - if asyncio.iscoroutine(self.media_bucket) - else self.media_bucket - ) + self._core_bucket = _return_or_await_and_return(self.core_bucket) + + self._persistence_bucket = _return_or_await_and_return(self.persistence_bucket) + + self._webhook = _return_or_await_and_return(self.webhook) + + self._rds_credentials = _return_or_await_and_return(self.rds_credentials) + + +@sync_compatible +async def _return_or_await_and_return(block: Block | Awaitable[Block]) -> Block: + """Return the block if it is already loaded, otherwise await and return.""" + if asyncio.iscoroutine(block): + return await block + return block blocks = Blocks() diff --git a/src/borderlands/cli/__init__.py b/src/borderlands/cli/__init__.py index d1481c6..4476f3e 100644 --- a/src/borderlands/cli/__init__.py +++ b/src/borderlands/cli/__init__.py @@ -6,6 +6,8 @@ from borderlands.cli.blocks import blocks from borderlands.cli.docs import docs +from borderlands.cli.rds import rds borderlands.add_command(blocks) borderlands.add_command(docs) +borderlands.add_command(rds) diff --git a/src/borderlands/cli/blocks.py b/src/borderlands/cli/blocks.py index e5cd1c1..f1809d7 100644 --- a/src/borderlands/cli/blocks.py +++ b/src/borderlands/cli/blocks.py @@ -248,6 +248,86 @@ def kaggle_credentials( save(kaggle_username, downstream=[partial(save, kaggle_key)]) +@blocks.command() +@click.option( + "-n", + "--database-name", + type=str, + default="borderlands", + help="The name of the database.", +) +@click.option( + "-u", + "--username", + type=str, + default="Prefect", + help="The username for the database.", +) +@click.option( + "-h", + "--host", + type=str, + default="env:DATABASE_HOST", + help="The host for the database. Prefix with 'env:' to use an environment variable.", +) +@click.option( + "-p", + "--port", + type=int, + default=3306, + help="The port for the database.", +) +@click.option( + "-c", + "--credentials", + type=str, + default="aws-credentials-prefect", + help="The name of the AWS credentials block. Prefix with 'env:' to use an environment variable.", +) +@click.option( + "-b", + "--block-name", + type=str, + default="rds-credentials-dev", + help="The name of the block.", +) +def rds_credentials( + database_name: str, + username: str, + host: str, + port: int, + credentials: str, + block_name: str, +): + """Create the RDS credentials block. The database and IAM user ought to be configured for AWS IAM authentication.""" + + if host.startswith("env:"): + env_var = host.split(":", maxsplit=1)[1] + host = os.environ[env_var] + + if credentials.startswith("env:"): + env_var = credentials.split(":", maxsplit=1)[1] + credentials = os.environ[env_var] + + from prefect_aws import AwsCredentials + from prefect_sqlalchemy import SyncDriver + + from borderlands.utilities.blocks import RdsCredentials + + iam_credentials = AwsCredentials.load(credentials) + + db_credentials = RdsCredentials( + _block_document_name=block_name, + driver=SyncDriver.MYSQL_MYSQLCONNECTOR, + database=database_name, + username=username, + host=host, + port=port, + iam_credentials=iam_credentials, + ) + save(db_credentials) + + @blocks.command() @click.option( "-u", diff --git a/src/borderlands/cli/rds.py b/src/borderlands/cli/rds.py new file mode 100644 index 0000000..3656c49 --- /dev/null +++ b/src/borderlands/cli/rds.py @@ -0,0 +1,45 @@ +""" +Module to manage RDS assets. +""" + +import os + +import click + + +@click.group() +def rds(): + """Manage RDS assets.""" + pass + + +@click.group() +def tables(): + """Manage RDS tables.""" + pass + + +rds.add_command(tables) + + +@tables.command() +@click.option( + "--credentials", + "-c", + default="database-credentials-admin", + type=str, + help="Credentials with permission to create the table.", +) +def equipment_loss(credentials: str): + """Create the EquipmentLoss table.""" + + os.environ["LIBMYSQL_ENABLE_CLEARTEXT_PLUGIN"] = "1" + + from prefect_sqlalchemy import DatabaseCredentials + + from borderlands.models import EquipmentLoss + + credentials: DatabaseCredentials = DatabaseCredentials.load(credentials) + engine = credentials.get_engine() + + EquipmentLoss.metadata.create_all(engine.engine, checkfirst=True) diff --git a/src/borderlands/enums.py b/src/borderlands/enums.py index d2f405b..7f6d9d3 100644 --- a/src/borderlands/enums.py +++ b/src/borderlands/enums.py @@ -19,3 +19,7 @@ class MediaType(enum.Enum): VIDEO = "video" GIF = "gif" UNKNOWN = "unknown" + + +class LossDateInferenceMethod(enum.StrEnum): + """The method used to infer the date of the equipment loss.""" diff --git a/src/borderlands/media.py b/src/borderlands/media.py index 2a7f42d..39f3ae6 100644 --- a/src/borderlands/media.py +++ b/src/borderlands/media.py @@ -18,7 +18,7 @@ from . import enums, paths from .blocks import blocks -from .definitions import EquipmentLoss, Media, media_inventory +from .definitions import Media, media_inventory from .schema import Tag from .utilities import io_, web, wrappers @@ -48,12 +48,12 @@ def create_media_inventory_from_oryx(df: pl.DataFrame) -> str: # Get the media from the evidence_url lf = ( - lf.groupby(EquipmentLoss.url_hash.name) + lf.groupby("url_hash") .agg( - EquipmentLoss.evidence_url.col.first().alias(Media.url.name), - EquipmentLoss.evidence_source.col.first().alias(Media.evidence_source.name), + pl.col("evidence_url").first().alias("url"), + pl.col("evidence_source").first().alias("evidence_source"), ) - .rename({EquipmentLoss.url_hash.name: Media.url_hash.name}) + .rename({"url_hash": "url_hash"}) ) # Fill the others with None null_fields = list(Media.iter(exclude=[Tag.inherited, Tag.dimension])) @@ -126,8 +126,8 @@ def create_media_key(ctx: dict) -> str: str: The media key. """ return ( - f"{ctx[Media.evidence_source.name]}/" - f"{ctx[Media.url_hash.name]}{ctx[Media.file_type.name] or '.unknown'}" + f"{ctx['evidence_source']}/" + f"{ctx['url_hash']}{ctx['file_type'] or '.unknown'}" ) @@ -147,8 +147,8 @@ def get_downloaded_and_not_downloaded( Returns: tuple[pl.DataFrame, pl.DataFrame]: The downloaded and not downloaded dataframes. """ - downloaded = df.filter(Media.media_key.col.is_not_null()) - not_downloaded = df.filter(Media.media_key.col.is_null()) + downloaded = df.filter(pl.col("media_key").is_not_null()) + not_downloaded = df.filter(pl.col("media_key").is_null()) return downloaded, not_downloaded @@ -173,31 +173,29 @@ async def download_file( try: try: await sem.acquire() - url = ctx[Media.url.name] + url = ctx["url"] async with client.stream("GET", url) as r: r.raise_for_status() # Infer the file type and with that the media type - ctx[Media.file_type.name] = io_.infer_media_extension( - ctx[Media.url.name], r.headers - ) - ctx[Media.media_type.name] = ( + ctx["file_type"] = io_.infer_media_extension(ctx["url"], r.headers) + ctx["media_type"] = ( enums.MediaType.IMAGE.value - if ctx[Media.file_type.name] + if ctx["file_type"] else enums.MediaType.UNKNOWN.value ) path = create_media_key(ctx) with tempfile.SpooledTemporaryFile( - prefix=ctx[Media.url_hash.name], suffix=".partial" + prefix=ctx["url_hash"], suffix=".partial" ) as fo: async for chunk in r.aiter_bytes(): fo.write(chunk) fo.seek(0) # Assign the media key and type - ctx[Media.media_key.name] = ( + ctx["media_key"] = ( await blocks.media_bucket.upload_from_file_object(fo, path) ) - ctx[Media.as_of_date.name] = datetime.datetime.utcnow() + ctx["as_of_date"] = datetime.datetime.utcnow() finally: sem.release() except httpx.HTTPStatusError as e: @@ -253,7 +251,7 @@ async def wrapper( logger = get_prefect_or_default_logger() logger.info(f"Searching for {evidence_source.value} media to download") - df = df.filter(Media.evidence_source.col == evidence_source.value) + df = df.filter(pl.col("evidence_source") == evidence_source.value) downloaded, not_downloaded = get_downloaded_and_not_downloaded(df) if not_downloaded.shape[0] == 0: @@ -309,10 +307,7 @@ async def download_postimg(contexts: list[dict], concurrency: int = 10): sem = anyio.Semaphore(concurrency) async with anyio.create_task_group() as tg: for ctx in contexts: - if ( - ctx[Media.evidence_source.name] - == enums.EvidenceSource.POST_IMG.value - ): + if ctx["evidence_source"] == enums.EvidenceSource.POST_IMG.value: tg.start_soon(download_file, client, ctx, sem) @@ -327,7 +322,7 @@ async def download(df: pl.DataFrame) -> pl.DataFrame: pl.DataFrame: The dataframe with the media keys. """ logger = get_prefect_or_default_logger() - evidence_sources = df[Media.evidence_source.name].unique().to_list() + evidence_sources = df["evidence_source"].unique().to_list() results: dict[str, pl.DataFrame] = {} async with anyio.create_task_group() as tg: for evidence_source in evidence_sources: @@ -337,6 +332,6 @@ async def download(df: pl.DataFrame) -> pl.DataFrame: else: logger.warning(f"No handler for {evidence_source}") results[evidence_source] = df.filter( - Media.evidence_source.col == evidence_source + pl.col("evidence_source") == evidence_source ) return pl.concat(results.values()) diff --git a/src/borderlands/models.py b/src/borderlands/models.py new file mode 100644 index 0000000..7701050 --- /dev/null +++ b/src/borderlands/models.py @@ -0,0 +1,166 @@ +""" +Database models for the datasets. +""" + +import datetime + +import polars as pl +import sqlalchemy as sa +from sqlmodel import Field, SQLModel + +from borderlands.enums import EvidenceSource + + +class EquipmentLoss(SQLModel, table=True): + """Data model for the equipment loss dataset.""" + + # Dimensions + country: str = Field( + primary_key=True, + title="Country", + description="The ISO Alpha-3 code of the country that lost the equipment.", + sa_type=sa.String(3), + ) + category: str = Field( + primary_key=True, + title="Category", + description="The equipment category as defined by Oryx.", + sa_type=sa.String(64), + ) + model: str = Field( + primary_key=True, + title="Model", + description="The equipment model as defined by Oryx.", + sa_type=sa.String(64), + ) + url_hash: str = Field( + primary_key=True, + title="URL Hash", + description="A SHA-256 hash of the `evidence_url`.", + sa_type=sa.String(64), + index=True, + ) + case_id: int = Field( + primary_key=True, + title="Case ID", + description=( + "A special ID for discriminating equipment losses when their `country`, `category`, `model`, and `url_hash` are the same." + " Order is determined by appearance in the source page." + ), + sa_type=sa.SmallInteger, + ) + + # Statuses + is_abandoned: bool = Field( + title="Abandoned", + description="The loss is due to abandonment.", + ) + is_captured: bool = Field( + title="Captured", + description="The loss is due to a belligerent capturing the equipment.", + ) + is_damaged: bool = Field( + title="Damaged", + description="The loss is due to damage.", + ) + is_destroyed: bool = Field( + title="Destroyed", + description="The loss is due to irrepairable destruction.", + ) + is_scuttled: bool = Field( + title="Scuttled", + description="The loss is due to scuttling. Only applies to naval equipment.", + ) + is_stripped: bool = Field( + title="Stripped", + description="The equipment was stripped of parts.", + ) + is_sunk: bool = Field( + title="Sunk", + description="The loss is due to sinking. Only applies to naval equipment.", + ) + is_raised: bool = Field( + title="Raised", + description="The equipment was raised from the sea floor.", + ) + + # Attributes + evidence_url: str = Field( + title="Evidence URL", + description="The URL to the evidence of the equipment loss. Derived from `oryx_evidence_url`.", + nullable=False, + sa_type=sa.String(512), + ) + country_of_production: str = Field( + title="Country of Production", + description="The ISO Alpha-3 code of the country that produces the equipment.", + sa_type=sa.String(3), + ) + evidence_source: EvidenceSource = Field( + None, + title="Evidence Source", + description="The source the Oryx loss references as evidence of the status.", + sa_type=sa.String(32), + ) + + # Source context + oryx_description: str | None = Field( + None, + title="Oryx Description", + description="The description of the equipment loss as provided by Oryx.", + ) + oryx_id: int | None = Field( + None, + title="Oryx ID", + description="The ID of the equipment loss as provided by Oryx. Unreliable due to duplicates.", + ) + oryx_evidence_url: str = Field( + title="Oryx Evidence URL", + description="The URL to the Oryx cites for the equipment loss.", + sa_type=sa.String(512), + ) + country_of_production_flag_url: str = Field( + None, + title="Country of Production Flag URL", + description="The URL to the flag of the country that produces the `model`.", + sa_type=sa.String(512), + ) + created_on: datetime.datetime = Field( + title="As of Date", + description="The date the row was added to the dataset.", + default_factory=datetime.datetime.now, + nullable=False, + ) + + @classmethod + def polars_schema(cls) -> dict[str, type[pl.DataType]]: + """Returns the Polars schema for the model.""" + return dict( + country=pl.Utf8, + category=pl.Utf8, + model=pl.Utf8, + url_hash=pl.Utf8, + case_id=pl.Int32, + is_abandoned=pl.Boolean, + is_captured=pl.Boolean, + is_damaged=pl.Boolean, + is_destroyed=pl.Boolean, + is_scuttled=pl.Boolean, + is_stripped=pl.Boolean, + is_sunk=pl.Boolean, + is_raised=pl.Boolean, + evidence_url=pl.Utf8, + country_of_production=pl.Utf8, + evidence_source=pl.Utf8, + date_of_loss=pl.Date, + country_of_production_flag_url=pl.Utf8, + oryx_description=pl.Utf8, + oryx_id=pl.Int32, + oryx_evidence_url=pl.Utf8, + date_loss_inference_method=pl.Utf8, + created_on=pl.Datetime, + ) + + def columns(self) -> list[str]: + """Returns the columns of the model.""" + return list(self.polars_schema().keys()) diff --git a/src/borderlands/oryx.py b/src/borderlands/oryx.py index 1b06381..a9618d9 100644 --- a/src/borderlands/oryx.py +++ b/src/borderlands/oryx.py @@ -6,6 +6,7 @@ import enum import hashlib import logging +import tempfile from urllib.parse import urlparse import bs4 @@ -18,10 +19,11 @@ from prefecto.filesystems import task_persistence_subfolder from prefecto.logging import get_prefect_or_default_logger from prefecto.serializers.polars import PolarsSerializer +from sqlmodel import Session from .blocks import blocks -from .definitions import EquipmentLoss from .enums import EvidenceSource +from .models import EquipmentLoss from .parser import article, parser from .utilities import web, wrappers @@ -64,8 +66,8 @@ async def alert_on_unmapped_country_flags(df: pl.DataFrame) -> None: """ logger = get_prefect_or_default_logger() unmapped: dict[str, str | int] = ( - df.filter(EquipmentLoss.country_of_production.col.is_null())[ - EquipmentLoss.country_of_production_flag_url.name + df.filter(pl.col("country_of_production").is_null())[ + "country_of_production_flag_url" ] .value_counts() .to_dicts() @@ -73,9 +75,7 @@ async def alert_on_unmapped_country_flags(df: pl.DataFrame) -> None: if unmapped: # Format the sections - urls = [ - case[EquipmentLoss.country_of_production_flag_url.name] for case in unmapped - ] + urls = [case["country_of_production_flag_url"] for case in unmapped] n, affected = len(urls), sum([case["counts"] for case in unmapped]) logger.warning( f"Found {n} unmapped country of production flags affecting {affected} records." @@ -114,7 +114,9 @@ async def alert_on_unmapped_country_flags(df: pl.DataFrame) -> None: class Status(enum.Enum): - """Statuses the equipment may be in.""" + """Statuses the equipment may be in. Names convert to model names by prefixing with 'is_' like + `is_abandoned`. + """ ABANDONED = "abandoned" CAPTURED = "captured" @@ -190,13 +192,21 @@ def parse_oryx_web_page(page: str, country: str | None = None) -> pl.DataFrame: generator = parser.OryxParser(soup, multi=country is None, logger=logger).parse( data_section_index ) - df = pl.from_dicts(generator, schema=EquipmentLoss.schema()) + df = pl.from_dicts(generator) logger.info(f"Found {len(df)} equipment losses for {country}") + df = df.rename( + { + "description": "oryx_description", + "evidence_url": "oryx_evidence_url", + "id_": "oryx_id", + } + ) + if country is not None: # Complete the country column df = df.with_columns( - pl.lit(country).alias(EquipmentLoss.country.name), + pl.lit(country).alias("country"), ) return df @@ -207,42 +217,21 @@ def assign_status(lf: pl.LazyFrame, *, logger: logging.Logger) -> pl.LazyFrame: """Assigns statuses to the equipment losses. Requires: - - `EquipmentLoss.description` + - `EquipmentLoss.oryx_description` """ logger.info("Assigning statuses to equipment losses") lf = lf.with_columns( pl.when( # Check if the description contains any of the keywords pl.any_horizontal( - EquipmentLoss.description.col.str.contains(keyword) - for keyword in keywords + pl.col("oryx_description").str.contains(keyword) for keyword in keywords ) ) - .then(pl.lit(status.value)) - .otherwise(pl.lit(None)) - .alias(status.value) + .then(pl.lit(True)) + .otherwise(pl.lit(False)) + .alias(f"is_{status.value}") for status, keywords in STATUS_KEYWORD_MAP.items() ) - - status_columns = [status.value for status in Status._member_map_.values()] - TMP = "tmp" - # Combine the status columns into a single column - lf = lf.with_columns( - # Concatenate the status columns into a list - pl.concat_list(pl.col(status) for status in status_columns) - # Use only unique values - # - This is only really applies to nulls. This way there is only one null in the list. Important for next. - .list.unique() - # Sort the list so that nulls are first - .list.sort().alias(TMP) - ).with_columns( - # If the first element of the list is null, drop it - pl.when(pl.col(TMP).list.first().is_null()) - .then(pl.col(TMP).list.slice(1, None)) - .otherwise(pl.col(TMP)) - .alias(EquipmentLoss.status.name) - ) - lf = lf.drop(status_columns + [TMP]) return lf @@ -259,9 +248,9 @@ def assign_country_of_production( logger.info("Assigning country of production flags to equipment losses") lf = lf.with_columns( - EquipmentLoss.country_of_production_flag_url.col.map_dict(mapper).alias( - EquipmentLoss.country_of_production.name - ) + pl.col("country_of_production_flag_url") + .map_dict(mapper) + .alias("country_of_production") ) return lf @@ -276,9 +265,10 @@ def assign_evidence_source(lf: pl.LazyFrame, *, logger: logging.Logger) -> pl.La """ logger.info("Assigning evidence sources to equipment losses") lf = lf.with_columns( - EquipmentLoss.evidence_url.col.apply(lambda x: urlparse(x).netloc) + pl.col("oryx_evidence_url") + .apply(lambda x: urlparse(x).netloc) .map_dict(DOMAIN_SOURCE_MAP) - .alias(EquipmentLoss.evidence_source.name) + .alias("evidence_source") ) return lf @@ -293,9 +283,9 @@ def calculate_url_hash(lf: pl.LazyFrame, *, logger: logging.Logger) -> pl.LazyFr """ logger.info("Calculating URL hashes") lf = lf.with_columns( - EquipmentLoss.evidence_url.col.apply( - lambda url: hashlib.sha256(url.encode("utf-8")).hexdigest() - ).alias(EquipmentLoss.url_hash.name) + pl.col("evidence_url") + .apply(lambda url: hashlib.sha256(url.encode("utf-8")).hexdigest()) + .alias("url_hash") ) return lf @@ -317,11 +307,11 @@ def resolve_aircraft_and_naval_page_updates( """Removes aircraft and naval losses that exist on the new pages.""" agg = ( lf.groupby( - EquipmentLoss.country.name, - EquipmentLoss.model.name, - EquipmentLoss.url_hash.name, + "country", + "model", + "url_hash", ) - .agg(EquipmentLoss.category.col.unique().alias("categories")) + .agg(pl.col("category").unique().alias("categories")) .with_columns( ( pl.col("categories").list.contains(pl.lit("Aircraft")) @@ -339,9 +329,9 @@ def resolve_aircraft_and_naval_page_updates( lf = lf.join( to_replace, on=[ - EquipmentLoss.country.name, - EquipmentLoss.model.name, - EquipmentLoss.url_hash.name, + "country", + "model", + "url_hash", ], how="left", ).filter( @@ -350,27 +340,31 @@ def resolve_aircraft_and_naval_page_updates( pl.col("to_replace").is_null() | ( pl.col("to_replace").is_not_null() - & EquipmentLoss.category.col.is_in(["Aircraft", "Naval Ships"]).is_not() + & pl.col("category").is_in(["Aircraft", "Naval Ships"]).is_not() ) ) - lf = ( - lf.join( - lookup, - left_on=[ - EquipmentLoss.category.name, - EquipmentLoss.model.name, - ], - right_on=["old_category", "model"], - how="left", - ) - .with_columns( - pl.when(pl.col("new_category").is_not_null()) - .then(pl.col("new_category")) - .otherwise(pl.col("category")) - .alias(EquipmentLoss.category.name), - ) - .drop("new_category") + lf = lf.join( + lookup, + left_on=[ + "category", + "model", + ], + right_on=["old_category", "model"], + how="left", + ).with_columns( + pl.when(pl.col("new_category").is_not_null()) + .then(pl.col("new_category")) + .otherwise(pl.col("category")) + .alias("category"), + ) + + lf = lf.drop( + "categories", + "from_original", + "new_category", + "pages_shared_on", + "to_replace", ) return lf @@ -399,17 +393,88 @@ def calculate_case_id(lf: pl.LazyFrame, *, logger: logging.Logger) -> pl.LazyFra To ensure that the two cases are not conflated, the case ID is used to discriminate between the two. """ logger.info("Calculating case IDs") - lf = lf.with_columns(pl.lit(1).alias(EquipmentLoss.case_id.name)).with_columns( - EquipmentLoss.case_id.col.cumsum().over( - EquipmentLoss.country.name, - EquipmentLoss.category.name, - EquipmentLoss.model.name, - EquipmentLoss.url_hash.name, + lf = lf.with_columns(pl.lit(1).alias("case_id")).with_columns( + pl.col("case_id") + .cumsum() + .over( + "country", + "category", + "model", + "url_hash", ), ) return lf +@wrappers.force_lazyframe +@wrappers.inject_default_logger +def generate_evidence_url(lf: pl.LazyFrame, *, logger: logging.Logger) -> pl.LazyFrame: + """Generates the evidence URL for the equipment losses. + + Requires: + - `EquipmentLoss.oryx_id` + """ + + logger.info("Generating evidence URLs") + # First populate with the general URL provided by Oryx + lf = lf.with_columns( + pl.col("oryx_evidence_url").alias("evidence_url"), + ) + + # Force http to https + lf = lf.with_columns( + pl.when(pl.col("evidence_url").str.starts_with("http://")) + .then(pl.col("evidence_url").str.replace(r"^http://", "https://", n=1)) + .otherwise(pl.col("evidence_url")) + .alias("evidence_url"), + ) + + # Remove / suffix from URLs + lf = lf.with_columns( + pl.col("evidence_url").str.replace(r"/$", "", n=1).alias("evidence_url"), + ) + + # Remove the www. prefix from URLs + lf = lf.with_columns( + pl.col("evidence_url") + .str.replace(r"^[^/]+?//(www\.)", "", n=1) + .alias("evidence_url"), + ) + + # For postimg URLs, remove the filename and image subdomain + # e.g. https://i.postimg.cc/vZQn2h5x/2001-t80bv-destr.jpg -> https://postimg.cc/vZQn2h5x + lf = lf.with_columns( + pl.when(pl.col("evidence_source") == EvidenceSource.POST_IMG.value) + .then( + pl.col("evidence_url") + .str.replace(r"i\.postimg\.cc", "postimg.cc") + .str.extract(r"(^.+\.cc/[^/]+?)/") + .fill_null(pl.col("evidence_url")) + ) + .alias("evidence_url"), + ) + return lf + + +@wrappers.force_lazyframe +@wrappers.inject_default_logger +def convert_country_to_iso(lf: pl.LazyFrame, *, logger: logging.Logger) -> pl.LazyFrame: + """Converts the country names to their ISO 3166-1 alpha-2 codes. + + Requires: + - `EquipmentLoss.country` + """ + logger.info("Converting country names to ISO codes") + return lf.with_columns( + pl.col("country").replace( + { + "Russia": "RUS", + "Ukraine": "UKR", + } + ), + ) + + @task_persistence_subfolder(blocks.persistence_bucket) @task( tags=["www.oryxspioenkop.com"], @@ -448,7 +513,7 @@ def pre_process_dataframe( # Add the as of date lf = ( lf.with_columns( - pl.lit(as_of_date, dtype=pl.Datetime).alias(EquipmentLoss.as_of_date.name), + pl.lit(as_of_date, dtype=pl.Datetime).alias("as_of_date"), ) .collect() .lazy() @@ -457,10 +522,10 @@ def pre_process_dataframe( # Clean strings lf = ( lf.with_columns( - EquipmentLoss.category.col.str.strip(), - EquipmentLoss.model.col.str.strip(), - EquipmentLoss.evidence_url.col.str.strip(), - EquipmentLoss.country_of_production_flag_url.col.str.strip(), + pl.col("category").str.strip(), + pl.col("model").str.strip(), + pl.col("oryx_evidence_url").str.strip(), + pl.col("country_of_production_flag_url").str.strip(), ) .collect() .lazy() @@ -472,11 +537,116 @@ def pre_process_dataframe( lf.pipe(assign_status) .pipe(assign_country_of_production, country_url_mapper) .pipe(assign_evidence_source) + .pipe(generate_evidence_url) .pipe(calculate_url_hash) .pipe(resolve_aircraft_and_naval_page_updates, category_corrections.lazy()) .pipe(calculate_case_id) + .pipe(convert_country_to_iso) ) .collect() .lazy() ) + + # Order columns + lf = lf.select( + "country", + "category", + "model", + "url_hash", + "case_id", + "is_abandoned", + "is_captured", + "is_damaged", + "is_destroyed", + "is_scuttled", + "is_stripped", + "is_sunk", + "is_raised", + "evidence_url", + "country_of_production", + "evidence_source", + "oryx_description", + "oryx_id", + "oryx_evidence_url", + "country_of_production_flag_url", + "as_of_date", + ) return lf.collect() + + +@task( + name="Load Oryx Equipment Loss to S3", + description="Loads a snapshot of the Oryx equipment loss data to S3 as an uncompressed CSV.", + retries=3, + retry_delay_seconds=exponential_backoff(3), + retry_jitter_factor=0.1, +) +def load_oryx_equipment_loss_to_s3(df: pl.DataFrame, path: str) -> str: + """Loads the Oryx equipment loss data to S3. + + Parameters + ---------- + df : pl.DataFrame + The data to load to S3. + path : str + The path to save the data to. + """ + with tempfile.NamedTemporaryFile(mode="w+b", suffix=".csv") as f: + df.write_csv(f) + f.seek(0) + return blocks.core_bucket.upload_from_file_object(f, path) + + +@task( + name="Load S3 Oryx Equipment Loss to Table", + description="Loads the Oryx equipment loss data from S3 to the production table.", + retries=3, + retry_delay_seconds=exponential_backoff(3), + retry_jitter_factor=0.1, + timeout_seconds=600, +) +def load_s3_equipment_loss_to_table(key: str): + """Loads the Oryx equipment loss data from S3 to a table.""" + + engine = blocks.rds_credentials.get_engine() + with Session(engine, autocommit=False) as session: + # Create and populate a staging table + with session.begin(): + # Create a temporary table + temp_table_name = f"{EquipmentLoss.__tablename__}_staging" + session.exec( + f""" + CREATE TEMPORARY TABLE {temp_table_name} LIKE {EquipmentLoss.__tablename__}; + """ + ) + # Load the data from S3 + session.exec( + """ + LOAD DATA FROM S3 '%(key)s' + INTO TABLE %(tmp)s + FIELDS TERMINATED BY ',' + LINES TERMINATED BY '\n' + IGNORE 1 LINES; + """, + params=dict( + key=key, + tmp=temp_table_name, + ), + ) + + # Filter for rows not in the target table and insert them + with session.begin(): + session.exec( + """ + INSERT INTO %(target)s + SELECT + staging.* + FROM %(tmp)s as staging + ANTIJOIN %(target)s as target + USING (country, category, model, url_hash, case_id); + """, + params=dict( + target=EquipmentLoss.__tablename__, + tmp=temp_table_name, + ), + ) diff --git a/src/borderlands/paths.py b/src/borderlands/paths.py index 74f45c6..796dacb 100644 --- a/src/borderlands/paths.py +++ b/src/borderlands/paths.py @@ -1,21 +1,227 @@ -"""""" +""" +Module for generating storage paths for the data lake. +""" -import datetime +from pathlib import Path -from .utilities import misc +# Creating reference since it's used as a func below +property_wrapper = property -def create_oryx_key(dt: datetime.datetime | None = None, ext: str | None = None) -> str: - """Creates the key for the Oryx equipment losses on this date. +class LakePath: + """A class for generating storage paths for the data lake. - Args: - dt (datetime.datetime, optional): The date the data was collected. 'latest' will be used if None. Defaults to None. - ext (str, optional): The file extension. Defaults to None. + Example: + + ```python + from borderlands.paths import LakePath + + LakePath().equipment_loss.date("2021-01-01").render() + # 'equipment-loss/date=2021-01-01' + + LakePath().evidence.source("postimg").url_hash("1234567890").join(".csv).render() + # 'evidence/source=postimg/url-hash=1234567890.csv' + ``` + + """ + + def __init__(self, root: Path | str | None = None) -> None: + """Initialize the LakePath object. + + Args: + root (Path | str | None, optional): The root path of the data lake. Defaults to None. + + """ + if isinstance(root, Path): + self.root = root + elif isinstance(root, str): + self.root = Path(root) + elif root is None: + self.root = Path("") + else: + raise TypeError(f"Invalid type for root: {type(root)}") + + @property_wrapper + def images(self) -> "LakePath": + """Return the path to the images data.""" + return LakePath(self.root / "images") + + @property_wrapper + def pages(self) -> "LakePath": + """Return the path to the pages data.""" + return LakePath(self.root / "pages") + + @property_wrapper + def processed(self) -> "LakePath": + """Return the path to the processed data.""" + return LakePath(self.root / "processed") + + @property_wrapper + def equipment_loss(self) -> "LakePath": + """Return the path to the equipment loss data.""" + return LakePath(self.root / "equipment-loss") + + @property_wrapper + def evidence(self) -> "LakePath": + """Return the path to the evidence data.""" + return LakePath(self.root / "evidence") + + def page(self, page: str) -> "LakePath": + """Return the path to the data for a specific page.""" + return LakePath(self.root / f"page={page}") + + def date(self, date: str) -> "LakePath": + """Return the path to the data for a specific date.""" + return LakePath(self.root / f"date={date}") + + def source(self, source: str) -> "LakePath": + """Return the path to the data for a specific source.""" + return LakePath(self.root / f"source={source}") + + def url_hash(self, url_hash: str) -> "LakePath": + """Return the path to the data for a specific URL hash.""" + return LakePath(self.root / f"url-hash={url_hash}") + + def render(self) -> str: + """Render the path as a string.""" + return self.root.as_posix() + + def join(self, *args: str) -> "LakePath": + """Join the path with additional elements.""" + return LakePath(self.root.joinpath(*args)) + + +class LakeNav: + """A collection of recipes for navigating the paths of the data lake. + + # Equipment Loss Data + + The equipment loss data is stored in the `equipment-loss` directory. This + data tends to be dumped and left alone in perpetuity. Most accesses + will be for testing and debugging purposes. Keeping the data for a run + on a specific date in a single directory makes it easy to find and + utilize related files. Rarely will a like file types be accessed or processed + together in bulk. + + ``` + equipment-loss/ + ├── date=2021-01-01 + │ ├── pages + │ │ ├── page=naval.html + │ │ └── page=aircraft.html + │ │ └── ... + │ └── processed + │ ├── oryx.csv + │ └── ... + └── ... + ``` + + # Evidence Data + + The evidence data is stored in the `evidence` directory. Different sources + will have fundamentally different data. For example, the postimg source + has information in both images and the body text of the page. However, + X data may include text, images, and/or video. Each source's data is + stored in a subdirectory of the evidence directory, and those subdirectories + are uniquely organized by the needs of the source. + + ``` + evidence/ + ├── source=postimg + │ ├── pages + │ │ ├── url-hash=1234567890.html + │ │ └── ... + │ └── images + │ ├── url-hash=1234567890.jpg + │ └── ... + └── ... + ``` """ - prefix: str = "" - if dt is None: - prefix = "latest" - else: - prefix = misc.build_datetime_key(dt, "month") + "/" + dt.strftime(r"%Y-%m-%d") - return prefix + (f".{ext.lstrip('.')}" if ext else "") + + def __init__(self, root: Path | str | None = None) -> None: + """Initialize the Recipes object.""" + self.lake_path = LakePath(root) + + def equipment_page(self, date: str, page: str, ext: str = "html") -> str: + """Return the path to the equipment loss page for a specific date. + + Args: + date (str): The date of the equipment loss page. + page (str): The page name. + ext (str): The file extension. + + Example: + + ```python + LakeNav().equipment_page("2021-01-01") + # 'equipment-loss/date=2021-01-01/pages/page=naval.html' + ``` + """ + ext = "." + ext.lstrip(".") + return self.lake_path.equipment_loss.date(date).pages.page(page + ext).render() + + def equipment_data(self, date: str, ext: str = "csv") -> str: + """Return the path to the equipment loss data for a specific date. + + Args: + date (str): The date of the equipment loss data. + ext (str): The file extension. + + Example: + + ```python + LakeNav().equipment_data("2021-01-01") + # 'equipment-loss/date=2021-01-01/processed/oryx.csv' + ``` + """ + ext = "." + ext.lstrip(".") + return ( + self.lake_path.equipment_loss.date(date) + .processed.join("oryx" + ext) + .render() + ) + + def evidence_page(self, source: str, url_hash: str, ext: str = "html") -> str: + """Return the path to the evidence page for a specific source and URL hash. + + Args: + source (str): The source of the evidence. + url_hash (str): The URL hash of the evidence. + ext (str): The file extension. + + Example: + + ```python + LakeNav().evidence_page("postimg", "1234567890") + # 'evidence/source=postimg/pages/url-hash=1234567890.html' + ``` + """ + ext = "." + ext.lstrip(".") + return ( + self.lake_path.evidence.source(source) + .pages.url_hash(url_hash + ext) + .render() + ) + + def evidence_img(self, source: str, url_hash: str, ext: str = "jpg") -> str: + """Return the path to the evidence image for a specific source and URL hash. + + Args: + source (str): The source of the evidence. + url_hash (str): The URL hash of the evidence. + ext (str): The file extension. + + Example: + + ```python + LakeNav().evidence_img("postimg", "1234567890") + # 'evidence/source=postimg/images/url-hash=1234567890.jpg' + ``` + """ + ext = "." + ext.lstrip(".") + return ( + self.lake_path.evidence.source(source) + .images.url_hash(url_hash + ext) + .render() + ) diff --git a/src/borderlands/utilities/blocks.py b/src/borderlands/utilities/blocks.py new file mode 100644 index 0000000..61d176e --- /dev/null +++ b/src/borderlands/utilities/blocks.py @@ -0,0 +1,47 @@ +""" +Custom blocks for the pipeline. +""" + +from prefect_aws import AwsCredentials +from prefect_sqlalchemy import DatabaseCredentials +from pydantic import VERSION as PYDANTIC_VERSION + +if PYDANTIC_VERSION.startswith("2."): + from pydantic.v1 import Field, SecretStr +else: + from pydantic import Field, SecretStr + +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import AsyncConnection + + +class RdsCredentials(DatabaseCredentials): + """A Prefect Block for storing AWS RDS credentials and connecting via IAM.""" + + _block_type_name = "RDS Credentials" + _logo_url = "https://icon.icepanel.io/AWS/svg/Database/RDS.svg" + + iam_credentials: AwsCredentials = Field( + title="IAM Credentials", + description="The AWS credentials to connect to the RDS instance with.", + ) + + def get_engine(self) -> Connection | AsyncConnection: + """Returns an authenticated engine that can be used to query the specified RDS database. + + Returns: + The authenticated SQLAlchemy Connection / AsyncConnection. + """ + client = self.iam_credentials.get_client("rds") + # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/rds/client/generate_db_auth_token.html + self.password = SecretStr( + client.generate_db_auth_token( + DBHostname=self.host, + Port=self.port, + DBUsername=self.username, + Region=self.iam_credentials.region_name, + ) + ) + self.block_initialization() + engine = super().get_engine() + return engine diff --git a/src/flows/oryx.py b/src/flows/oryx.py index cbcacda..d85486b 100644 --- a/src/flows/oryx.py +++ b/src/flows/oryx.py @@ -3,60 +3,40 @@ """ import datetime -import io -import polars as pl -from prefect import flow, task +from prefect import flow from prefect.context import FlowRunContext, get_run_context -from borderlands import assets, definitions +from borderlands import assets from borderlands.blocks import blocks from borderlands.oryx import ( alert_on_unmapped_country_flags, get_oryx_page, + load_oryx_equipment_loss_to_s3, + load_s3_equipment_loss_to_table, parse_oryx_web_page, pre_process_dataframe, ) -from borderlands.paths import create_oryx_key +from borderlands.paths import LakeNav from borderlands.utilities import tasks -@task -def upload(df: pl.DataFrame, dt: datetime.datetime) -> str: - """Uploads the DataFrame to S3. - - Args: - df (pl.DataFrame): The DataFrame to upload. - dt (datetime.datetime): The datetime to use for the key. - - Returns: - str: The key the DataFrame was uploaded to. - """ - key = create_oryx_key(dt, ext="parquet") - df = df.select(definitions.EquipmentLoss.columns()) - df = df.sort(definitions.EquipmentLoss.columns(include=[definitions.Tag.dimension])) - with io.BytesIO() as buffer: - df.write_parquet(buffer, compression="zstd", compression_level=22) - buffer.seek(0) - blob = buffer.read() - return tasks.upload.fn(content=blob, key=key, bucket=blocks.oryx_bucket) - - @flow( name="Oryx Flow", description=( "Flow to extract the equipment loss data from the Russian" "and Ukrainian loss pages on https://www.oryxspioenkop.com/." ), - # No reason this should take more than 10 minutes. Most runs will be < 30 - # seconds - timeout_seconds=600, + timeout_seconds=1800, log_prints=True, ) -def oryx_flow() -> str: +def oryx_flow(prefix: str | None = None) -> str: """Flow to retrieve the web pages of Russian and Ukrainian equipment losses and parse them into processable JSON documents. + Args: + prefix (str, optional): The prefix to use for the S3 key. Defaults to None. + Returns: str: The key the DataFrame was uploaded to. """ @@ -91,4 +71,8 @@ def oryx_flow() -> str: ) df = pre_process_dataframe(df, mapper, category_corrections, dt) alert_on_unmapped_country_flags(df) - return upload(df, dt) + + # Generate the key + key = LakeNav(prefix).equipment_data(dt.strftime("%Y-%m-%d")) + stage_key = load_oryx_equipment_loss_to_s3(df, key) + load_s3_equipment_loss_to_table(stage_key) diff --git a/tests/conftest.py b/tests/conftest.py index 83a6a37..bc0a378 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,13 +2,9 @@ Pytest configuration. """ -import gzip -import json -import shutil from pathlib import Path from typing import TYPE_CHECKING -import bs4 import pytest from _pytest.monkeypatch import MonkeyPatch from prefect import task @@ -19,14 +15,10 @@ from prefecto.testing.s3 import mock_bucket if TYPE_CHECKING: - from borderlands.parser.article import ArticleParser + pass TESTS_PATH: Path = Path(__file__).parent -EXPORT_PATH: Path = TESTS_PATH / "export" -if EXPORT_PATH.is_dir(): - shutil.rmtree(EXPORT_PATH) -EXPORT_PATH.mkdir(exist_ok=True) @pytest.fixture @@ -62,7 +54,7 @@ def slack_webhook() -> SlackWebhook: ) -@pytest.fixture(autouse=True, scope="session") +@pytest.fixture(scope="session") def prefect_db(slack_webhook, credentials, core_bucket, persistence_bucket): """Sets the Prefect test harness for local pipeline testing.""" with prefect_test_harness(): @@ -84,39 +76,6 @@ def mock_buckets( yield -@pytest.fixture -def oryx_descriptions(test_data_path: Path) -> list[str]: - """Descriptions of the Oryx articles.""" - with open(test_data_path / "descriptions.txt", "r") as fo: - return [line.strip() for line in fo.readlines()] - - -@pytest.fixture -def oryx_evidence_urls(test_data_path: Path) -> list[str]: - """URLs of the Oryx evidence.""" - with open(test_data_path / "evidence_urls.txt", "r") as fo: - return [line.strip() for line in fo.readlines()] - - -@pytest.fixture -def oryx_flag_urls(test_data_path: Path) -> list[str]: - """URLs of the Oryx flags.""" - with open(test_data_path / "country_of_production_flag_urls.txt", "r") as fo: - return [line.strip() for line in fo.readlines()] - - -@pytest.fixture -def flag_url_mapper(test_data_path: Path) -> dict[str, str]: - """URLs of the Oryx flags.""" - with open( - test_data_path - / "buckets/borderlands-core/assets/country_of_production_url_mapping.json", - "r", - ) as fo: - data = json.load(fo) - return {k: v["Alpha-3"] for k, v in data.items()} - - @pytest.fixture(autouse=False, scope="function") def mock_slack_webhook(monkeypatch: MonkeyPatch): """Mocks the Slack webhook.""" @@ -133,112 +92,3 @@ async def mock_send_incoming_webhook_message(*args, **kwds): mock_send_incoming_webhook_message, ) yield - - -@pytest.fixture(autouse=False, scope="function") -def mock_oryx_page_request(test_data_path: Path, monkeypatch: MonkeyPatch): - """Mock the Cvent API to prevent API rate limit excession.""" - import requests - - def mock_request_page(url: str, *args, **kwds) -> requests.Response: - """Mocks the extract_pages function to use a file in the tests/data directory.""" - filename: str | None = None - if "www.oryxspioenkop.com" in url: - folder = test_data_path / "pages" - if ( - url - == "https://www.oryxspioenkop.com/2022/02/attack-on-europe-documenting-equipment.html" - ): - filename = "russia.html.gz" - elif ( - url - == "https://www.oryxspioenkop.com/2022/02/attack-on-europe-documenting-ukrainian.html" - ): - filename = "ukraine.html.gz" - elif ( - url - == "https://www.oryxspioenkop.com/2022/03/list-of-naval-losses-during-2022.html" - ): - filename = "naval.html.gz" - elif ( - url - == "https://www.oryxspioenkop.com/2022/03/list-of-aircraft-losses-during-2022.html" - ): - filename = "aircraft.html.gz" - else: - raise ValueError(f"URL not mocked: {url}") - - with gzip.open(folder / filename, "rb") as f: - response = requests.Response() - response.status_code = 200 - response._content = f.read() - return response - - elif "postimg.cc" in url: - # https://i.postimg.cc/vm6DrVLL/h79.jpg - # -> images/vm6DrVLL-h79.jpg - parts = url.split("/") - filename = f"{parts[-2]}-{parts[-1]}" - folder = test_data_path / "images" - # These are being streamed in chunks - response = requests.Response() - response.status_code = 200 - response.raw = open(folder / filename, "rb") - return response - else: - raise ValueError(f"URL not mocked: {url}") - - monkeypatch.setattr(requests, "get", mock_request_page) - yield - - -@pytest.fixture -def oryx_ukraine_webpage(test_data_path: Path) -> bs4.Tag: - """Path to the Oryx Ukraine webpage.""" - with gzip.open(test_data_path / "pages" / "ukraine.html.gz", "r") as fo: - return bs4.BeautifulSoup(fo.read(), features="html.parser") - - -@pytest.fixture -def oryx_russia_webpage(test_data_path: Path) -> bs4.Tag: - """Path to the Oryx Ukraine webpage.""" - with gzip.open(test_data_path / "pages" / "russia.html.gz", "r") as fo: - return bs4.BeautifulSoup(fo.read(), features="html.parser") - - -@pytest.fixture -def ukraine_article_parser(oryx_ukraine_webpage: bs4.Tag) -> "ArticleParser": - """An `ArticleParser` object.""" - from borderlands.parser.article import ArticleParser - - body = oryx_ukraine_webpage.find( - attrs={"class": "post-body entry-content", "itemprop": "articleBody"} - ) - from borderlands.parser.article import UKRAINE_DATA_SECTION_INDEX - - yield ArticleParser(body, UKRAINE_DATA_SECTION_INDEX) - - -@pytest.fixture -def russia_article_parser(oryx_russia_webpage: bs4.Tag) -> "ArticleParser": - """An `ArticleParser` object.""" - from borderlands.parser.article import ArticleParser - - body = oryx_russia_webpage.find( - attrs={"class": "post-body entry-content", "itemprop": "articleBody"} - ) - from borderlands.parser.article import RUSSIA_DATA_SECTION_INDEX - - yield ArticleParser(body, RUSSIA_DATA_SECTION_INDEX) - - -@pytest.fixture -def ukraine_page_parse_result(ukraine_article_parser: "ArticleParser") -> list: - """The result of parsing the Ukraine page.""" - yield list(ukraine_article_parser.parse()) - - -@pytest.fixture -def russia_page_parse_result(russia_article_parser: "ArticleParser") -> list: - """The result of parsing the Russia page.""" - yield list(russia_article_parser.parse()) diff --git a/tests/test_blocks.py b/tests/test_blocks.py new file mode 100644 index 0000000..cdd150e --- /dev/null +++ b/tests/test_blocks.py @@ -0,0 +1,27 @@ +import pytest +from prefect.blocks.system import String + +from borderlands.blocks import ( + Blocks, + _return_or_await_and_return, +) + + +class TestBlocks: + def test_init_env(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("BLOCKS_CORE_BUCKET_NAME", "core-bucket") + + blocks = Blocks() + assert blocks.core_bucket_name == "core-bucket" + + +def test__return_or_await_and_return(prefect_db): + block: String = String( + _block_document_name="string-test", + value="test string", + ) + block.save() + + result = _return_or_await_and_return(String.load("string-test")) + assert isinstance(result, String) + assert result.value == "test string" diff --git a/tests/test_flows.py b/tests/test_flows.py index 1113b32..6a31d7f 100644 --- a/tests/test_flows.py +++ b/tests/test_flows.py @@ -2,11 +2,76 @@ Tests for the Oryx flow. """ +import gzip +from pathlib import Path + +import httpx import pytest +@pytest.fixture +def mock_httpx_func(test_data_path: Path): + + def _mock_httpx_func(url: str, *args, **kwds) -> httpx.Response: + + filename: str | None = None + if "www.oryxspioenkop.com" in url: + folder = test_data_path / "pages" + if ( + url + == "https://www.oryxspioenkop.com/2022/02/attack-on-europe-documenting-equipment.html" + ): + filename = "russia.html.gz" + elif ( + url + == "https://www.oryxspioenkop.com/2022/02/attack-on-europe-documenting-ukrainian.html" + ): + filename = "ukraine.html.gz" + elif ( + url + == "https://www.oryxspioenkop.com/2022/03/list-of-naval-losses-during-2022.html" + ): + filename = "naval.html.gz" + elif ( + url + == "https://www.oryxspioenkop.com/2022/03/list-of-aircraft-losses-during-2022.html" + ): + filename = "aircraft.html.gz" + else: + raise ValueError(f"URL not mocked: {url}") + + with gzip.open(folder / filename, "rb") as f: + response = httpx.Response( + status_code=200, + content=f.read(), + request=httpx.Request("GET", url), + ) + return response + + elif "postimg.cc" in url: + # https://i.postimg.cc/vm6DrVLL/h79.jpg + # -> images/vm6DrVLL-h79.jpg + parts = url.split("/") + filename = f"{parts[-2]}-{parts[-1]}" + folder = test_data_path / "images" + + # These are being streamed in chunks + response = httpx.Response( + status_code=200, + content=open(folder / filename, "rb"), + request=httpx.Request("GET", url), + ) + return response + else: + raise ValueError(f"URL not mocked: {url}") + + yield _mock_httpx_func + + @pytest.mark.skip(reason="Causes crash in CI.") -def test_oryx_flow(mock_buckets, mock_oryx_page_request, mock_slack_webhook): +def test_oryx_flow( + prefect_db, mock_buckets, mock_oryx_page_request, mock_slack_webhook +): """Test the Oryx equipment loss staging flow.""" from flows.oryx import oryx_flow @@ -21,6 +86,7 @@ def test_oryx_flow(mock_buckets, mock_oryx_page_request, mock_slack_webhook): @pytest.mark.skip(reason="Causes crash in CI.") def test_download_media( + prefect_db, mock_buckets, mock_oryx_page_request, ): diff --git a/tests/test_oryx.py b/tests/test_oryx.py new file mode 100644 index 0000000..b0b8587 --- /dev/null +++ b/tests/test_oryx.py @@ -0,0 +1,59 @@ +import logging + +import polars as pl +import pytest + +from borderlands.oryx import assign_status + + +@pytest.fixture(scope="module") +def logger(): + return logging.getLogger(__name__) + + +def test_assign_status(logger): + df = pl.DataFrame( + [ + ["was captured"], + ["was destroyed"], + ["was damaged"], + ["was damagd"], + ["was abandoned"], + ["was abanonded"], + ["was scuttled"], + ["was stripped"], + ["was sunk"], + ["was raised"], + ], + schema=dict(oryx_description=pl.Utf8), + ) + result = assign_status(df.lazy(), logger=logger).collect() + EXPECTED_COLUMNS = [ + "oryx_description", + "is_captured", + "is_destroyed", + "is_damaged", + "is_abandoned", + "is_scuttled", + "is_stripped", + "is_sunk", + "is_raised", + ] + assert result.columns == EXPECTED_COLUMNS + assert result.melt( + id_vars=["oryx_description"], + value_vars=EXPECTED_COLUMNS[1:], + variable_name="status", + value_name="is_status", + ).filter(pl.col("is_status"))["status"].to_list() == [ + "is_captured", + "is_destroyed", + "is_damaged", + "is_damaged", + "is_abandoned", + "is_abandoned", + "is_scuttled", + "is_stripped", + "is_sunk", + "is_raised", + ] diff --git a/tests/test_parser.py b/tests/test_parser.py index 75f9fc7..b694094 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -2,12 +2,69 @@ Tests for the article parser. """ +import gzip +from pathlib import Path from typing import TYPE_CHECKING +import bs4 +import pytest + if TYPE_CHECKING: from borderlands.parser.article import ArticleParser +@pytest.fixture +def oryx_ukraine_webpage(test_data_path: Path) -> bs4.Tag: + """Path to the Oryx Ukraine webpage.""" + with gzip.open(test_data_path / "pages" / "ukraine.html.gz", "r") as fo: + return bs4.BeautifulSoup(fo.read(), features="html.parser") + + +@pytest.fixture +def oryx_russia_webpage(test_data_path: Path) -> bs4.Tag: + """Path to the Oryx Ukraine webpage.""" + with gzip.open(test_data_path / "pages" / "russia.html.gz", "r") as fo: + return bs4.BeautifulSoup(fo.read(), features="html.parser") + + +@pytest.fixture +def ukraine_article_parser(oryx_ukraine_webpage: bs4.Tag) -> "ArticleParser": + """An `ArticleParser` object.""" + from borderlands.parser.article import ArticleParser + + body = oryx_ukraine_webpage.find( + attrs={"class": "post-body entry-content", "itemprop": "articleBody"} + ) + from borderlands.parser.article import UKRAINE_DATA_SECTION_INDEX + + yield ArticleParser(body, UKRAINE_DATA_SECTION_INDEX) + + +@pytest.fixture +def russia_article_parser(oryx_russia_webpage: bs4.Tag) -> "ArticleParser": + """An `ArticleParser` object.""" + from borderlands.parser.article import ArticleParser + + body = oryx_russia_webpage.find( + attrs={"class": "post-body entry-content", "itemprop": "articleBody"} + ) + from borderlands.parser.article import RUSSIA_DATA_SECTION_INDEX + + yield ArticleParser(body, RUSSIA_DATA_SECTION_INDEX) + + +@pytest.fixture +def ukraine_page_parse_result(ukraine_article_parser: "ArticleParser") -> list: + """The result of parsing the Ukraine page.""" + yield list(ukraine_article_parser.parse()) + + +@pytest.fixture +def russia_page_parse_result(russia_article_parser: "ArticleParser") -> list: + """The result of parsing the Russia page.""" + yield list(russia_article_parser.parse()) + + def test_ukraine_page_parse(ukraine_article_parser: "ArticleParser"): """Tests parsing the Ukraine page.""" assert ukraine_article_parser