diff --git a/CHANGELOG. b/CHANGELOG. deleted file mode 100644 index e69de29..0000000 diff --git a/Makefile b/Makefile index 19a45d4..38f68d0 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,16 @@ .PHONY: update-deps update-deps: - pip install --upgrade pip-tools pip setuptools - pip-compile --upgrade --build-isolation --generate-hashes --output-file requirements/main.txt requirements/main.in - pip-compile --upgrade --build-isolation --generate-hashes --output-file requirements/dev.txt requirements/dev.in + pip install --upgrade uv + uv pip compile --upgrade --build-isolation --generate-hashes --output-file requirements/main.txt requirements/main.in + uv pip compile --upgrade --build-isolation --generate-hashes --output-file requirements/dev.txt requirements/dev.in .PHONY: init init: - pip install --upgrade pip setuptools wheel - pip install --editable . - pip install --upgrade -r requirements/main.txt -r requirements/dev.txt + pip install --upgrade uv + uv pip install --editable . + uv pip install --upgrade -r requirements/main.txt -r requirements/dev.txt rm -rf .tox - pip install --upgrade tox + uv pip install --upgrade tox pre-commit install .PHONY: update diff --git a/requirements/dev.in b/requirements/dev.in index 3f07157..5a9452e 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -2,8 +2,13 @@ # Type checking mypy +types-pyyaml + +# Linting +ruff +pre-commit # Testing pytest +pytest-asyncio pytest-cov - diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..5dedd5b --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,255 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --build-isolation --generate-hashes --output-file requirements/dev.txt requirements/dev.in +cfgv==3.4.0 \ + --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ + --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 + # via pre-commit +coverage==7.6.1 \ + --hash=sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca \ + --hash=sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d \ + --hash=sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6 \ + --hash=sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989 \ + --hash=sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c \ + --hash=sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b \ + --hash=sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223 \ + --hash=sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f \ + --hash=sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56 \ + --hash=sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3 \ + --hash=sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8 \ + --hash=sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb \ + --hash=sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388 \ + --hash=sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0 \ + --hash=sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a \ + --hash=sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8 \ + --hash=sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f \ + --hash=sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a \ + --hash=sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962 \ + --hash=sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8 \ + --hash=sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391 \ + --hash=sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc \ + --hash=sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2 \ + --hash=sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155 \ + --hash=sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb \ + --hash=sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0 \ + --hash=sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c \ + --hash=sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a \ + --hash=sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004 \ + --hash=sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060 \ + --hash=sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232 \ + --hash=sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93 \ + --hash=sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129 \ + --hash=sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163 \ + --hash=sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de \ + --hash=sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6 \ + --hash=sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23 \ + --hash=sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569 \ + --hash=sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d \ + --hash=sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778 \ + --hash=sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d \ + --hash=sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36 \ + --hash=sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a \ + --hash=sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6 \ + --hash=sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34 \ + --hash=sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704 \ + --hash=sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106 \ + --hash=sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9 \ + --hash=sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862 \ + --hash=sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b \ + --hash=sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255 \ + --hash=sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16 \ + --hash=sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3 \ + --hash=sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133 \ + --hash=sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb \ + --hash=sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657 \ + --hash=sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d \ + --hash=sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca \ + --hash=sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36 \ + --hash=sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c \ + --hash=sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e \ + --hash=sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff \ + --hash=sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7 \ + --hash=sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5 \ + --hash=sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02 \ + --hash=sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c \ + --hash=sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df \ + --hash=sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3 \ + --hash=sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a \ + --hash=sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959 \ + --hash=sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234 \ + --hash=sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc + # via pytest-cov +distlib==0.3.8 \ + --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ + --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64 + # via virtualenv +filelock==3.15.4 \ + --hash=sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb \ + --hash=sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7 + # via virtualenv +identify==2.6.0 \ + --hash=sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf \ + --hash=sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0 + # via pre-commit +iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + # via pytest +mypy==1.11.1 \ + --hash=sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54 \ + --hash=sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a \ + --hash=sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72 \ + --hash=sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69 \ + --hash=sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b \ + --hash=sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe \ + --hash=sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4 \ + --hash=sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd \ + --hash=sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0 \ + --hash=sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525 \ + --hash=sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2 \ + --hash=sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c \ + --hash=sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5 \ + --hash=sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de \ + --hash=sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74 \ + --hash=sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c \ + --hash=sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e \ + --hash=sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58 \ + --hash=sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b \ + --hash=sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417 \ + --hash=sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411 \ + --hash=sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb \ + --hash=sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03 \ + --hash=sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca \ + --hash=sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8 \ + --hash=sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08 \ + --hash=sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809 + # via -r requirements/dev.in +mypy-extensions==1.0.0 \ + --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ + --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 + # via mypy +nodeenv==1.9.1 \ + --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ + --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 + # via pre-commit +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 + # via pytest +platformdirs==4.2.2 \ + --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \ + --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3 + # via virtualenv +pluggy==1.5.0 \ + --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ + --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 + # via pytest +pre-commit==3.8.0 \ + --hash=sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af \ + --hash=sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f + # via -r requirements/dev.in +pytest==8.3.2 \ + --hash=sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5 \ + --hash=sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce + # via + # -r requirements/dev.in + # pytest-asyncio + # pytest-cov +pytest-asyncio==0.23.8 \ + --hash=sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2 \ + --hash=sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3 + # via -r requirements/dev.in +pytest-cov==5.0.0 \ + --hash=sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652 \ + --hash=sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857 + # via -r requirements/dev.in +pyyaml==6.0.2 \ + --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ + --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ + --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ + --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ + --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ + --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ + --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ + --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ + --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ + --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ + --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ + --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ + --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ + --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ + --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ + --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ + --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ + --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ + --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ + --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ + --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ + --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ + --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ + --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ + --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ + --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ + --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ + --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ + --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ + --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ + --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ + --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ + --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ + --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ + --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ + --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ + --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ + --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ + --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ + --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ + --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ + --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ + --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ + --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ + --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ + --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ + --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ + --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ + --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ + --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ + --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ + --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 + # via + # -c requirements/main.txt + # pre-commit +ruff==0.5.7 \ + --hash=sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be \ + --hash=sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3 \ + --hash=sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4 \ + --hash=sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a \ + --hash=sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499 \ + --hash=sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a \ + --hash=sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb \ + --hash=sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc \ + --hash=sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e \ + --hash=sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5 \ + --hash=sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5 \ + --hash=sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf \ + --hash=sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8 \ + --hash=sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e \ + --hash=sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692 \ + --hash=sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e \ + --hash=sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e \ + --hash=sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea + # via -r requirements/dev.in +types-pyyaml==6.0.12.20240808 \ + --hash=sha256:b8f76ddbd7f65440a8bda5526a9607e4c7a322dc2f8e1a8c405644f9a6f4b9af \ + --hash=sha256:deda34c5c655265fc517b546c902aa6eed2ef8d3e921e4765fe606fe2afe8d35 + # via -r requirements/dev.in +typing-extensions==4.12.2 \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 + # via + # -c requirements/main.txt + # mypy +virtualenv==20.26.3 \ + --hash=sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a \ + --hash=sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589 + # via pre-commit diff --git a/requirements/main.txt b/requirements/main.txt new file mode 100644 index 0000000..cc49dff --- /dev/null +++ b/requirements/main.txt @@ -0,0 +1,341 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --build-isolation --generate-hashes --output-file requirements/main.txt requirements/main.in +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 + # via pydantic +anyio==4.4.0 \ + --hash=sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94 \ + --hash=sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7 + # via + # httpx + # starlette +certifi==2024.7.4 \ + --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \ + --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90 + # via + # httpcore + # httpx +cffi==1.17.0 \ + --hash=sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f \ + --hash=sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab \ + --hash=sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499 \ + --hash=sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058 \ + --hash=sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693 \ + --hash=sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb \ + --hash=sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377 \ + --hash=sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885 \ + --hash=sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2 \ + --hash=sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401 \ + --hash=sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4 \ + --hash=sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b \ + --hash=sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59 \ + --hash=sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f \ + --hash=sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c \ + --hash=sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555 \ + --hash=sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa \ + --hash=sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424 \ + --hash=sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb \ + --hash=sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2 \ + --hash=sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8 \ + --hash=sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e \ + --hash=sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9 \ + --hash=sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82 \ + --hash=sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828 \ + --hash=sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759 \ + --hash=sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc \ + --hash=sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118 \ + --hash=sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf \ + --hash=sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932 \ + --hash=sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a \ + --hash=sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29 \ + --hash=sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206 \ + --hash=sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2 \ + --hash=sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c \ + --hash=sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c \ + --hash=sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0 \ + --hash=sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a \ + --hash=sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195 \ + --hash=sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6 \ + --hash=sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9 \ + --hash=sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc \ + --hash=sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb \ + --hash=sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0 \ + --hash=sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7 \ + --hash=sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb \ + --hash=sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a \ + --hash=sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492 \ + --hash=sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720 \ + --hash=sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42 \ + --hash=sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7 \ + --hash=sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d \ + --hash=sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d \ + --hash=sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb \ + --hash=sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4 \ + --hash=sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2 \ + --hash=sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b \ + --hash=sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8 \ + --hash=sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e \ + --hash=sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204 \ + --hash=sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3 \ + --hash=sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150 \ + --hash=sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4 \ + --hash=sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76 \ + --hash=sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e \ + --hash=sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb \ + --hash=sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91 + # via cryptography +click==8.1.7 \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de + # via safir +cryptography==43.0.0 \ + --hash=sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709 \ + --hash=sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069 \ + --hash=sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2 \ + --hash=sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b \ + --hash=sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e \ + --hash=sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70 \ + --hash=sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778 \ + --hash=sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22 \ + --hash=sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895 \ + --hash=sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf \ + --hash=sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431 \ + --hash=sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f \ + --hash=sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947 \ + --hash=sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74 \ + --hash=sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc \ + --hash=sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66 \ + --hash=sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66 \ + --hash=sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf \ + --hash=sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f \ + --hash=sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5 \ + --hash=sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e \ + --hash=sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f \ + --hash=sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55 \ + --hash=sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1 \ + --hash=sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47 \ + --hash=sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5 \ + --hash=sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0 + # via + # pyjwt + # safir +fastapi==0.112.0 \ + --hash=sha256:3487ded9778006a45834b8c816ec4a48d522e2631ca9e75ec5a774f1b052f821 \ + --hash=sha256:d262bc56b7d101d1f4e8fc0ad2ac75bb9935fec504d2b7117686cec50710cf05 + # via safir +gidgethub==5.3.0 \ + --hash=sha256:4dd92f2252d12756b13f9dd15cde322bfb0d625b6fb5d680da1567ec74b462c0 \ + --hash=sha256:9ece7d37fbceb819b80560e7ed58f936e48a65d37ec5f56db79145156b426a25 + # via safir +h11==0.14.0 \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 + # via httpcore +httpcore==1.0.5 \ + --hash=sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61 \ + --hash=sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5 + # via httpx +httpx==0.27.0 \ + --hash=sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5 \ + --hash=sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5 + # via safir +idna==3.7 \ + --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ + --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 + # via + # anyio + # httpx +pycparser==2.22 \ + --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ + --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc + # via cffi +pydantic==2.8.2 \ + --hash=sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a \ + --hash=sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8 + # via + # fastapi + # safir +pydantic-core==2.20.1 \ + --hash=sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d \ + --hash=sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f \ + --hash=sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686 \ + --hash=sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482 \ + --hash=sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006 \ + --hash=sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83 \ + --hash=sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6 \ + --hash=sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88 \ + --hash=sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86 \ + --hash=sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a \ + --hash=sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6 \ + --hash=sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a \ + --hash=sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6 \ + --hash=sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6 \ + --hash=sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43 \ + --hash=sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c \ + --hash=sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4 \ + --hash=sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e \ + --hash=sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203 \ + --hash=sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd \ + --hash=sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1 \ + --hash=sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24 \ + --hash=sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc \ + --hash=sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc \ + --hash=sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3 \ + --hash=sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598 \ + --hash=sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98 \ + --hash=sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331 \ + --hash=sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2 \ + --hash=sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a \ + --hash=sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6 \ + --hash=sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688 \ + --hash=sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91 \ + --hash=sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa \ + --hash=sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b \ + --hash=sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0 \ + --hash=sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840 \ + --hash=sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c \ + --hash=sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd \ + --hash=sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3 \ + --hash=sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231 \ + --hash=sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1 \ + --hash=sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953 \ + --hash=sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250 \ + --hash=sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a \ + --hash=sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2 \ + --hash=sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20 \ + --hash=sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434 \ + --hash=sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab \ + --hash=sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703 \ + --hash=sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a \ + --hash=sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2 \ + --hash=sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac \ + --hash=sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611 \ + --hash=sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121 \ + --hash=sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e \ + --hash=sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b \ + --hash=sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09 \ + --hash=sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906 \ + --hash=sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9 \ + --hash=sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7 \ + --hash=sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b \ + --hash=sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987 \ + --hash=sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c \ + --hash=sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b \ + --hash=sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e \ + --hash=sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237 \ + --hash=sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1 \ + --hash=sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19 \ + --hash=sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b \ + --hash=sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad \ + --hash=sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0 \ + --hash=sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94 \ + --hash=sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312 \ + --hash=sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f \ + --hash=sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669 \ + --hash=sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1 \ + --hash=sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe \ + --hash=sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99 \ + --hash=sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a \ + --hash=sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a \ + --hash=sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52 \ + --hash=sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c \ + --hash=sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad \ + --hash=sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1 \ + --hash=sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a \ + --hash=sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f \ + --hash=sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a \ + --hash=sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27 + # via + # pydantic + # safir +pyjwt==2.9.0 \ + --hash=sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850 \ + --hash=sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c + # via gidgethub +pyyaml==6.0.2 \ + --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ + --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ + --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ + --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ + --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ + --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ + --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ + --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ + --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ + --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ + --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ + --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ + --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ + --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ + --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ + --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ + --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ + --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ + --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ + --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ + --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ + --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ + --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ + --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ + --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ + --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ + --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ + --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ + --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ + --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ + --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ + --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ + --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ + --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ + --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ + --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ + --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ + --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ + --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ + --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ + --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ + --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ + --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ + --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ + --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ + --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ + --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ + --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ + --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ + --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ + --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ + --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 + # via -r requirements/main.in +safir==6.2.0 \ + --hash=sha256:335219abba8ed663395bcf6cf86a60ec8de8412ea212dc0dbe8425e9faa7bc97 \ + --hash=sha256:61cf6fd3839c0945bcc7c01469dc8fcd19351eba33b6022c596684d87763e50e + # via -r requirements/main.in +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via + # anyio + # httpx +starlette==0.37.2 \ + --hash=sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee \ + --hash=sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823 + # via + # fastapi + # safir +structlog==24.4.0 \ + --hash=sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610 \ + --hash=sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4 + # via safir +typing-extensions==4.12.2 \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 + # via + # fastapi + # pydantic + # pydantic-core +uritemplate==4.1.1 \ + --hash=sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0 \ + --hash=sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e + # via gidgethub diff --git a/src/rsp_scratchpurger/__init__.py b/src/rsp_scratchpurger/__init__.py index e69de29..f7dbed4 100644 --- a/src/rsp_scratchpurger/__init__.py +++ b/src/rsp_scratchpurger/__init__.py @@ -0,0 +1,3 @@ +from .purger import Purger + +__all__ = ["Purger"] diff --git a/src/rsp_scratchpurger/cli.py b/src/rsp_scratchpurger/cli.py index d414c5c..b1a92ec 100644 --- a/src/rsp_scratchpurger/cli.py +++ b/src/rsp_scratchpurger/cli.py @@ -1,48 +1,129 @@ -"""Command-line interface for Google Filestore tools.""" -import argparse -import asyncio -import os +"""Command-line interface for purger.""" + from pathlib import Path +import click +import yaml +from pydantic import ValidationError +from safir.asyncio import run_with_asyncio +from safir.click import display_help +from safir.logging import LogLevel, Profile + +from .constants import CONFIG_FILE, ENV_PREFIX +from .models.config import Config from .purger import Purger -from .constants import POLICY_FILE, ENV_PREFIX - - -def _add_options() -> argparse.ArgumentParser: - """Add options applicable to any filestore tool.""" - parser = argparse.ArgumentParser() - parser.add_argument( - "-f", - "--file", - "--policy-file", - help="Policy file for purger", - default=os.environ.get(f"{ENV_PREFIX}FILE", POLICY_FILE), - type=Path, - required=True, - ) - parser.add_argument( - "-x", - "--dry-run", - help="Do not perform actions, but print what would be done", - type=bool, - default=bool(os.environ.get(f"{ENV_PREFIX}DRY_RUN", "")), - ) - parser.add_argument( - "-d", - "--debug", - "--verbose", - default=bool(os.environ.get(f"{ENV_PREFIX}DEBUG", "")), - type=bool, - help="Verbose debugging output", + + +@click.group(context_settings={"help_option_names": ["-h", "--help"]}) +@click.version_option(message="%(version)s") +def main() -> None: + """Command-line interface for purger.""" + + +@main.command() +@click.pass_context +def help(ctx: click.Context, topic: str | None) -> None: + """Show help for any command.""" + display_help(main, ctx, topic) + + +config_option = click.option( + "-c", + "--config-file", + "--config", + envvar=ENV_PREFIX + "CONFIG_FILE", + type=click.Path(path_type=Path), + help="Purger application configuration file", +) +policy_option = click.option( + "-p", + "--policy-file", + "--policy", + envvar=ENV_PREFIX + "POLICY_FILE", + type=click.Path(path_type=Path), + help="Purger policy file", +) +debug_option = click.option( + "-d", + "--debug", + envvar=ENV_PREFIX + "DEBUG", + type=bool, + help="Enable debug logging", +) +dry_run_option = click.option( + "-x", + "--dry-run", + envvar=ENV_PREFIX + "DRY_RUN", + type=bool, + help="Dry run: take no action, just emit what would be done.", +) + + +def _get_config( + config_file: Path | None = None, + policy_file: Path | None = None, + debug: bool | None = None, + dry_run: bool | None = None, +) -> Config: + try: + if config_file is None: + config_file = CONFIG_FILE + c_obj = yaml.safe_load(config_file.read_text()) + config = Config.model_validate(c_obj) + except (FileNotFoundError, ValidationError): + config = Config() + if policy_file is not None: + config.policy_file = policy_file + if debug is not None: + config.logging.log_level = LogLevel.DEBUG + config.logging.profile = Profile.development + if dry_run is not None: + config.dry_run = dry_run + return config + + +@config_option +@policy_option +@debug_option +@dry_run_option +@run_with_asyncio +async def report( + *, + config_file: Path | None, + policy_file: Path | None, + debug: bool | None, + dry_run: bool | None, +) -> None: + """Report what would be purged.""" + config = _get_config( + config_file=config_file, + policy_file=policy_file, + debug=debug, + dry_run=dry_run, ) - return parser - -def purge() -> None: - """Purge the target filesystems.""" - args = _get_options().parse_args() - purger = Purger( - policy_file=args.policy, - dry_run=args.dry_run, - debug=args.debug + purger = Purger(config=config) + await purger.plan() + await purger.report() + + +@config_option +@policy_option +@debug_option +@dry_run_option +async def purge( + *, + config_file: Path | None, + policy_file: Path | None, + debug: bool | None, + dry_run: bool | None, +) -> None: + """Report what would be purged.""" + config = _get_config( + config_file=config_file, + policy_file=policy_file, + debug=debug, + dry_run=dry_run, ) - asyncio.run(purger.purge()) + purger = Purger(config=config) + await purger.plan() + await purger.purge() diff --git a/src/rsp_scratchpurger/constants.py b/src/rsp_scratchpurger/constants.py index 3996bf2..d35ebaf 100644 --- a/src/rsp_scratchpurger/constants.py +++ b/src/rsp_scratchpurger/constants.py @@ -2,6 +2,7 @@ from pathlib import Path -ENV_PREFIX="RSP_SCRATCHPURGER_" -POLICY_FILE=Path("/etc/purger/config.yaml") - +CONFIG_FILE = Path("/etc/purger/config.yaml") +ENV_PREFIX = "RSP_SCRATCHPURGER_" +POLICY_FILE = Path("/etc/purger/policy.yaml") +ROOT_LOGGER = "rsp_scratchpurger" diff --git a/src/rsp_scratchpurger/exceptions.py b/src/rsp_scratchpurger/exceptions.py new file mode 100644 index 0000000..fefe131 --- /dev/null +++ b/src/rsp_scratchpurger/exceptions.py @@ -0,0 +1,11 @@ +"""Exceptions for the purger.""" + +from safir.slack.blockkit import SlackException + + +class PlanNotReadyError(SlackException): + """An operation needing a Plan was requested, but no Plan is ready.""" + + +class PolicyNotFoundError(SlackException): + """No Policy matching the given directory was found.""" diff --git a/src/rsp_scratchpurger/models/config.py b/src/rsp_scratchpurger/models/config.py index 1b53bee..dc1ca44 100644 --- a/src/rsp_scratchpurger/models/config.py +++ b/src/rsp_scratchpurger/models/config.py @@ -1,18 +1,74 @@ +"""Application configuration for the purger.""" + from pathlib import Path +from typing import Annotated +from pydantic import Field, HttpUrl +from safir.logging import LogLevel, Profile from safir.pydantic import CamelCaseModel -from pydantic import Field +from ..constants import ENV_PREFIX, POLICY_FILE + + +class LoggingConfig(CamelCaseModel): + """Configuration for the purger's logs.""" + + profile: Annotated[ + Profile, + Field( + title="Logging profile", + validation_alias=ENV_PREFIX + "LOGGING_PROFILE", + ), + ] = Profile.production + + log_level: Annotated[ + LogLevel, + Field(title="Log level", validation_alias=ENV_PREFIX + "LOG_LEVEL"), + ] = LogLevel.INFO + + add_timestamp: Annotated[ + bool, + Field( + title="Add timestamp to log lines", + validation_alias=ENV_PREFIX + "ADD_TIMESTAMP", + ), + ] = False -from typing import Annotated class Config(CamelCaseModel): + """Top-level configuration for the purger.""" + + policy_file: Annotated[ + Path, + Field( + title="Policy file location", + validation_alias=ENV_PREFIX + "POLICY_FILE", + ), + ] = POLICY_FILE - policy_file: Annotated[Path, Field(title="Policy file location")] + dry_run: Annotated[ + bool, + Field( + title="Report rather than execute plan", + validation_alias=ENV_PREFIX + "DRY_RUN", + ), + ] = False - dry_run: Annotated[bool, Field(title="Report rather than execute plan", - default=False)] + logging: Annotated[ + LoggingConfig, + Field( + title="Logging configuration", + ), + ] = LoggingConfig() - debug: Annotated[bool, Field(title="Verbose debugging output", - default=False)] - + alert_hook: Annotated[ + HttpUrl | None, + Field( + title="Slack webhook URL used for sending alerts", + description=( + "An https URL, which should be considered secret." + " If not set or set to `None`, this feature will be disabled." + ), + validation_alias=ENV_PREFIX + "ALERT_HOOK", + ), + ] = None diff --git a/src/rsp_scratchpurger/models/plan.py b/src/rsp_scratchpurger/models/plan.py new file mode 100644 index 0000000..79b77e2 --- /dev/null +++ b/src/rsp_scratchpurger/models/plan.py @@ -0,0 +1,46 @@ +"""Object representing files to be purged, and why.""" + +from enum import StrEnum +from pathlib import Path +from typing import Annotated + +from pydantic import Field +from safir.pydantic import CamelCaseModel + + +class FileClass(StrEnum): + """Whether a file is large or small.""" + + LARGE = "LARGE" + SMALL = "SMALL" + + +class FileReason(StrEnum): + """Whether a file is to be purged on access, creation, or modification + time grounds. + """ + + ATIME = "ATIME" + CTIME = "CTIME" + MTIME = "MTIME" + + +class FileRecord(CamelCaseModel): + """A file to be purged, and why.""" + + path: Annotated[Path, Field(..., title="Path for file to purge.")] + + file_class: Annotated[ + FileClass, Field(..., title="Class of file to purge (large or small).") + ] + + file_reason: Annotated[ + FileReason, + Field(..., title="Reason to purge file (access or creation time)."), + ] + + +class Plan(CamelCaseModel): + """List of files to be purged, and why.""" + + files: Annotated[list[FileRecord], Field(..., title="Files to purge")] diff --git a/src/rsp_scratchpurger/models/v1/policy.py b/src/rsp_scratchpurger/models/v1/policy.py index 304be8b..8bae7e8 100644 --- a/src/rsp_scratchpurger/models/v1/policy.py +++ b/src/rsp_scratchpurger/models/v1/policy.py @@ -1,28 +1,55 @@ +"""Model for purger policy.""" + +from dataclasses import dataclass from pathlib import Path +from typing import Annotated, TypeAlias -from safir.pydantic import CamelCaseModel, HumanTimeDelta +from pydantic import BeforeValidator, Field +from safir.pydantic import CamelCaseModel, HumanTimedelta -from pydantic import Field, field_validator, model_validator -from typing import Annotated, TypeAlias +@dataclass +class MantissaAndMultiplier: + """Utility for intermediate results in HumanSizeBytes conversion.""" + + mantissa: str + multiplier: float -def _validate_human_size_bytes(v: str | float | int) -> int: + +def _validate_human_size_bytes(v: str | float) -> int: + if isinstance(v, int): + return v if isinstance(v, float): if int(v) == v: return int(v) raise ValueError("Could not convert {v} to integer") - if not isinstance(v, str): - return v orig_v = v v = v.strip() # remove leading/trailing whitespace - if v.endswith("B" or "b"): # "b" is incorrect but we'll take it. + if v.endswith(("B", "b")): # "b" is incorrect but common. v = v[:-1] v = v.strip() # In case it was something like '42 B' try: return int(v) # Maybe it's just a stringified int? except ValueError: pass # Nope, try to convert it. - mult = 1 + mam = _extract_base_and_mult_from_string(v) + try: + n_v = float(mam.mantissa) + m_v = n_v * mam.multiplier + # Cheating: we will just round nearest if mult is not a power of 10. + if mam.multiplier % 10 != 0: + m_v = int(m_v + 0.5) + if m_v != int(m_v): + # Otherwise we complain that it's not an integer, to catch + # Way Too Much Precision (e.g. "1.234567 KiB") + raise ValueError # Caught immediately and reraised with text. + return int(m_v) + except ValueError: + raise ValueError(f"Could not convert '{orig_v}' to integer") from None + + +def _extract_base_and_mult_from_string(v: str) -> MantissaAndMultiplier: + mult = 1.0 # The things in the map turn out to be floats. # Since we require Python 3.12, this dict is ordered as shown. mult_map = { "k": 1e3, @@ -31,35 +58,27 @@ def _validate_human_size_bytes(v: str | float | int) -> int: "G": 1e9, "T": 1e12, "P": 1e15, - "E": 1e18 - "ki": 2 ** 10, - "Ki": 2 ** 10, # also incorrect. - "Mi": 2 ** 20, - "Gi": 2 ** 30, - "Ti": 2 ** 40, - "Pi": 2 ** 50, - "Ei": 2 ** 60 + "E": 1e18, + "ki": 2**10, + "Ki": 2**10, # also incorrect. + "Mi": 2**20, + "Gi": 2**30, + "Ti": 2**40, + "Pi": 2**50, + "Ei": 2**60, } suffixes = list(mult_map.keys()) for s in suffixes: if v.endswith(s): - v = v[:-len(s)] + v = v[: -len(s)] mult = mult_map[s] break v = v.strip() - try: - n_v = float(v) - m_v = n_v * mult - if m_v != int(m_v): - raise ValueError() # Caught immediately and reraised with text. - return int(m_v) - except ValueError: - raise ValueError(f"Could not convert '{orig_v}' to integer") - + return MantissaAndMultiplier(mantissa=v, multiplier=mult) + HumanSizeBytes: TypeAlias = Annotated[ - int, - BeforeValidator(_validate_human_size_bytes) + int, BeforeValidator(_validate_human_size_bytes) ] """Parse an input indicating a number of bytes into an int. @@ -96,58 +115,103 @@ def _validate_human_size_bytes(v: str | float | int) -> int: a ValueError is raised indicating the string could not be converted. """ + class Intervals(CamelCaseModel): - """This specifies how long it must have been since a filesystem object - was accessed or created before that object will be considered for - purging. + """Intervals specify how long it must have been since a filesystem object + was accessed, created, or modified before that object will be considered + for purging. A value of None (or a zero TimeDelta) means the object will + not be considered for purging on the given grounds. """ - access_interval: Annotated[HumanTimeDelta, Field( - title="Maximum time since last file access" - )] - creation_interval: Annotated[HumanTimeDelta, Field( - title="Maximum time since file creation" - )] + access_interval: Annotated[ + HumanTimedelta | None, + Field(title="Maximum time since last file access"), + ] = None + + creation_interval: Annotated[ + HumanTimedelta | None, Field(title="Maximum time since file creation") + ] = None + + modification_interval: Annotated[ + HumanTimedelta | None, + Field(title="Maximum time since file modification"), + ] = None + + def to_dict(self) -> dict[str, int]: + ret: dict[str, int] = { + "access_interval": 0, + "creation_interval": 0, + "modification_interval": 0, + } + if self.access_interval is not None: + ret["access_interval"] = int(self.access_interval.total_seconds()) + if self.creation_interval is not None: + ret["creation_interval"] = int( + self.creation_interval.total_seconds() + ) + if self.modification_interval is not None: + # The things we do for linters. + ret["modification_interval"] = int( + self.modification_interval.total_seconds() + ) + return ret + class SizedIntervals(CamelCaseModel): """Container to hold intervals for purging `large` and `small` files.""" - large: Annotated[Interval, Field( - title="Intervals before purging large files" - )] - - small: Annotated[Interval, Field( - title="Intervals before purging small files" - )] - + large: Annotated[ + Intervals, Field(title="Intervals before purging large files") + ] = Intervals() + + small: Annotated[ + Intervals, Field(title="Intervals before purging small files") + ] = Intervals() + + def to_dict(self) -> dict[str, dict[str, int]]: + return {"large": self.large.to_dict(), "small": self.small.to_dict()} + class DirectoryPolicy(CamelCaseModel): - """This specifies a policy for deletion of objects from a directory.""" + """Policy for purging objects from a directory and its children.""" path: Annotated[Path, Field(title="Directory to consider for purging")] - threshold: Annotated[int, Field( - title="Size in bytes demarcating `large` from `small` files" - )] + threshold: Annotated[ + HumanSizeBytes, + Field(title="Size in bytes demarcating `large` from `small` files"), + ] + + intervals: Annotated[ + SizedIntervals, + Field(title="Intervals before purging `large` and `small` files"), + ] + + def to_dict(self) -> dict[str, str | int | dict[str, dict[str, int]]]: + return { + "path": str(self.path), + "threshold": self.threshold, + "intervals": self.intervals.to_dict(), + } + - intervals: Annotated[SizedIntervals, Field( - title="Intervals before purging `large` and `small` files" - )] - class Policy(CamelCaseModel): - directories: Annotated[list[DirectoryPolicy], Field( - title="Directories specified in this policy" - )] - - def get_directories(self) -> list[name]: - """Return list of directory names specified in this policy, sorted by - length.""" - return [x.path.name for x in self._directories].sort( - lambda x: len(x) + """Policy for purging objects across multiple directory trees.""" + + directories: Annotated[ + list[DirectoryPolicy], + Field(title="Directories specified in this policy"), + ] + + def get_directories(self) -> list[Path]: + """Return list of directories specified in this policy, sorted by + length, shortest first. + """ + return sorted( + [x.path for x in self.directories], key=lambda x: len(str(x)) ) - - - - - + def to_dict( + self, + ) -> dict[str, list[dict[str, str | int | dict[str, dict[str, int]]]]]: + return {"directories": [x.to_dict() for x in self.directories]} diff --git a/src/rsp_scratchpurger/purger.py b/src/rsp_scratchpurger/purger.py index 0d66a46..8b29226 100644 --- a/src/rsp_scratchpurger/purger.py +++ b/src/rsp_scratchpurger/purger.py @@ -1,20 +1,215 @@ -"""The Purger class reads its policy document and provides mechanisms for -planning actions, reporting its plans, and executing its plans. +"""The Purger class provides mechanisms for setting its policy, +planning actions according to its policy, reporting its plans, and +executing its plans. """ +import asyncio +import datetime +from pathlib import Path + +import structlog import yaml +from safir.logging import configure_logging +from safir.slack.blockkit import SlackTextBlock -from pathlib import Path +from .constants import ROOT_LOGGER +from .exceptions import PlanNotReadyError, PolicyNotFoundError +from .models.config import Config +from .models.plan import FileClass, FileReason, FileRecord, Plan +from .models.v1.policy import DirectoryPolicy, Policy -from .constants import POLICY_FILE class Purger: + """Object to plan and execute filesystem purges.""" def __init__( - self, - policy_file: Path = POLICY_FILE, - dry_run:bool = False, - debug: bool = False + self, config: Config, logger: structlog.BoundLogger | None = None ) -> None: - self._plan: - + self._config = config + if logger is None: + self._logger = structlog.get_logger(ROOT_LOGGER) + configure_logging( + name=ROOT_LOGGER, + profile=config.logging.profile, + log_level=config.logging.log_level, + add_timestamp=config.logging.add_timestamp, + ) + else: + self._logger = logger + self._logger.debug("Purger initialized") + self._lock = asyncio.Lock() + self._plan: Plan | None = None + + def set_policy_file(self, policy_file: Path) -> None: + old = self._config.policy_file + self._config.policy_file = policy_file + self._logger.debug(f"Reset policy file: '{old}' -> '{policy_file}'") + + async def plan(self) -> None: + """Scan our directories and assemble a plan. We can only do this + when an operation is not in progress, hence the lock. + """ + self._logger.debug("Attempting to acquire lock for plan()") + async with self._lock: + self._logger.debug("Lock for plan() acquired.") + + self._logger.debug( + f"Reloading policy from {self._config.policy_file}" + ) + policy_doc = yaml.safe_load(self._config.policy_file.read_text()) + policy = Policy.model_validate(policy_doc) + + # Invalidate any current plan + self._plan = None + + directories = policy.get_directories() + + visited: list[Path] = [] + + # Set time at beginning of run + now = datetime.datetime.now(tz=datetime.UTC) + purge: list[FileRecord] = [] + while directories: + # Take a directory (the longest remaining) off the end + # of the list, and consider it. + consider = directories.pop() + self._logger.debug(f"Considering {consider!s}") + for root, dirs, files in consider.walk(): + # Grab the policy. + current_policy = self._get_directory_policy( + path=root, policy=policy + ) + # Filter out any we already visited. Yes, + # Path.walk() lets you modify the list dirs inside + # the loop. Creepy but handy for exactly this + # pruning task. + remove_dirs = self._filter_out(dirs, visited) + for rem in remove_dirs: + self._logger.debug(f"Pruning {rem}") + try: + dirs.remove(str(rem)) + except ValueError: + # We shouldn't get here, but I'm hazy on how + # that pruning really works. + self._logger.exception( + f"Tried to remove {rem!s} " + "; not in list to consider." + ) + + # Check each file. + for file in files: + purge_file = self._check_file( + path=root / file, policy=current_policy, when=now + ) + if purge_file is not None: + self._logger.debug( + f"Adding {purge_file} to purge list" + ) + purge.append(purge_file) + # OK, we're done with this tree. Skip it when + # considering higher (shorter-named) directories. + visited.insert(0, consider) + + self._plan = Plan(files=purge) + + def _get_directory_policy( + self, path: Path, policy: Policy + ) -> DirectoryPolicy: + for d_policy in policy.directories: + if d_policy.path == path: + return d_policy + raise PolicyNotFoundError(f"Policy for '{path}' not found") + + def _filter_out(self, dirs: list[str], visited: list[Path]) -> list[Path]: + # I'm sure there's an elegant way to do this as a double + # comprehension + remove: list[Path] = [] + for vis in visited: + remove.extend([Path(x) for x in dirs if x.startswith(str(vis))]) + return list(set(remove)) # Remove any duplicates + + def _check_file( + self, path: Path, policy: DirectoryPolicy, when: datetime.datetime + ) -> FileRecord | None: + st = path.stat() + # Get large-or-small policy, depending. + size = st.st_size + if size >= policy.threshold: + ivals = policy.intervals.large + f_class = FileClass.LARGE + else: + ivals = policy.intervals.small + f_class = FileClass.SMALL + atime = datetime.datetime.fromtimestamp(st.st_atime, tz=datetime.UTC) + ctime = datetime.datetime.fromtimestamp(st.st_ctime, tz=datetime.UTC) + mtime = datetime.datetime.fromtimestamp(st.st_mtime, tz=datetime.UTC) + a_max = ivals.access_interval + c_max = ivals.creation_interval + m_max = ivals.modification_interval + + # Check the file against the intervals + if a_max and (atime + a_max < when): + return FileRecord( + path=path, file_class=f_class, file_reason=FileReason.ATIME + ) + if c_max and (ctime + c_max < when): + return FileRecord( + path=path, file_class=f_class, file_reason=FileReason.CTIME + ) + if m_max and (mtime + m_max < when): + return FileRecord( + path=path, file_class=f_class, file_reason=FileReason.MTIME + ) + return None + + async def report(self) -> None: + """Report what directories are to be purged.""" + if self._plan is None: + raise PlanNotReadyError("Cannot report: plan not ready") + self._logger.debug("Awaiting lock for report()") + async with self._lock: + self._logger.debug("Acquired lock for report()") + if self._config.alert_hook is not None: + rpt_text = [ + f"{x.path!s}: {x.file_reason}" for x in self._plan.files + ] + if len(rpt_text) == 0: + rpt_text = ["No files to be purged"] + rpt_msg = SlackTextBlock( + heading="Purge plan", + text="\n".join(rpt_text), # May be truncated + ) + self._logger.info(rpt_msg) + else: + # Just log the plan. + self._logger.info({"plan": self._plan}) + + async def purge(self) -> None: + """Purge files and after-purge-empty directories.""" + if self._config.dry_run: + self._logger.warning( + "Cannot purge because dry_run enabled; reporting instead" + ) + await self.report() + return + if self._plan is None: + raise PlanNotReadyError("Cannot purge: plan not ready") + self._logger.debug("Awaiting lock for purge()") + async with self._lock: + self._logger.debug("Acquired lock for purge()") + victim_dirs: set[Path] = set() + for purge_file in self._plan.files: + path = purge_file.path + self._logger.debug(f"Removing {path!s}") + path.unlink() + victim_dirs.add(path.parent) + self._logger.debug("File purge complete; removing empty dirs") + vd_l = sorted( + list(victim_dirs), key=lambda x: len(str(x)), reverse=True + ) + for victim in vd_l: + if len(list(victim.glob("*"))) == 0: + self._logger.debug(f"Removing directory {victim!s}") + victim.rmdir() + self._logger.debug("Purge complete") + self._plan = None diff --git a/tests/cli_test.py b/tests/cli_test.py deleted file mode 100644 index 98f52ba..0000000 --- a/tests/cli_test.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Test the CLI.""" - -import subprocess - - -def test_backup_cli() -> None: - cp = subprocess.run("purge_backups", check=False) - assert cp.returncode == 2 # It won't have the arguments diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..fe99725 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,41 @@ +"""Pytest configuration and fixtures.""" + +from collections.abc import Iterator +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest +import yaml +from rsp_scratchpurger.models.config import Config +from rsp_scratchpurger.models.v1.policy import Policy + + +@pytest.fixture +def purger_config() -> Iterator[Config]: + policy_file = Path(__file__).parent / "support" / "policy.yaml" + policy_doc = yaml.safe_load(policy_file.read_text()) + policy = Policy.model_validate(policy_doc) + config_file = Path(__file__).parent / "support" / "config.yaml" + config_doc = yaml.safe_load(config_file.read_text()) + config = Config.model_validate(config_doc) + + with TemporaryDirectory() as temp_dir_str: + temp_dir = Path(temp_dir_str) + scratch_dir = temp_dir / "scratch" + scratch_foobar = scratch_dir / "foobar" + scratch_foobar.mkdir(parents=True) + + policy.directories[0].path = scratch_dir + policy.directories[1].path = scratch_foobar + + new_policy_dict = policy.to_dict() + new_policy_doc = yaml.dump(new_policy_dict) + + new_policy_file = temp_dir / "policy.yaml" + + new_policy_file.write_text(new_policy_doc) + + config = Config() + config.policy_file = new_policy_file + + yield config diff --git a/tests/import_test.py b/tests/import_test.py index 9dbc20c..2c05e39 100644 --- a/tests/import_test.py +++ b/tests/import_test.py @@ -1,7 +1,8 @@ """Test basic module functionality.""" import rsp_scratchpurger +from rsp_scratchpurger.models.config import Config -def test_import() -> None: - p = rsp_scratchpurger.Purger() +def test_import(purger_config: Config) -> None: + p = rsp_scratchpurger.purger.Purger(config=purger_config) assert p is not None diff --git a/tests/support/config.yaml b/tests/support/config.yaml new file mode 100644 index 0000000..c40aa0b --- /dev/null +++ b/tests/support/config.yaml @@ -0,0 +1,4 @@ +policy_file: /etc/purger/policy.yaml +dry_run: false +logging: + log_level: debug diff --git a/tests/support/policy.yaml b/tests/support/policy.yaml new file mode 100644 index 0000000..9ce7f2b --- /dev/null +++ b/tests/support/policy.yaml @@ -0,0 +1,20 @@ +directories: + - path: /scratch + threshold: 10B + intervals: + small: + access_interval: 30d + creation_interval: 365d + modification_interval: 90d + large: + access_interval: 1h + creation_interval: 24h + modification_interval: 4h + - path: /scratch/foobar + threshold: 20B + intervals: + large: + access_interval: 3s + creation_interval: 10s + modification_interval: 5s + diff --git a/tox.ini b/tox.ini index 81714f5..85a7471 100644 --- a/tox.ini +++ b/tox.ini @@ -26,14 +26,14 @@ commands = pre-commit run --all-files [testenv:py] commands = - pytest -vv {posargs} --cov=scratchpurger + pytest -vv {posargs} --cov=rsp_scratchpurger [testenv:py-coverage] description = Run pytest with coverage analysis commands = - pytest -vv --cov=scratchpurger --cov-branch --cov-report= {posargs} + pytest -vv --cov=rsp_scratchpurger --cov-branch --cov-report= {posargs} [testenv:typing] description = Run mypy commands = - mypy src/scratchpurger tests + mypy src/rsp_scratchpurger tests