diff --git a/Pipfile.lock b/Pipfile.lock index 94f49bc3..6a638532 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -186,19 +186,19 @@ }, "anthropic": { "hashes": [ - "sha256:83e06b3d9d40ff5898f588020e0cc4e42187de954549a3b5fbe6e2685a09c785", - "sha256:ce7d94a7657f2aa29338cca448945eac621b4f62c1794cf461cb32847223e9b8" + "sha256:39cbda0ac17a6d423e5bf609811bd69b26eddf6299d7a468126e05bc711ce826", + "sha256:c14edb36ed80da9099acbd26b5cec810d76606c31f32a0d56a4cf9d4fa9e25ae" ], "markers": "python_version >= '3.9'", - "version": "==0.109.1" + "version": "==0.111.0" }, "anyio": { "hashes": [ - "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", - "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc" + "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", + "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9" ], "markers": "python_version >= '3.10'", - "version": "==4.13.0" + "version": "==4.14.0" }, "attrs": { "hashes": [ @@ -218,11 +218,11 @@ }, "certifi": { "hashes": [ - "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", - "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d" + "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", + "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db" ], "markers": "python_version >= '3.7'", - "version": "==2026.5.20" + "version": "==2026.6.17" }, "cffi": { "hashes": [ @@ -552,12 +552,12 @@ }, "fastapi": { "hashes": [ - "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", - "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab" + "sha256:b6f54fd1bd72c80b0f899f172c61a600f6f7af9b43d4d772a018f35624048cb0", + "sha256:d445a4877636ad191e7053e08c9bf98cb921a6756776848400bb773d1740c061" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==0.136.3" + "version": "==0.138.0" }, "filetype": { "hashes": [ @@ -707,20 +707,20 @@ "requests" ], "hashes": [ - "sha256:130f6fd5e3f497fdad897a23ed9489973437edf561238c4b92a4d02c435f8af9", - "sha256:784e9837f92244141250470d47c893df50cbab485ce491aca5e9deb558ad2b48" + "sha256:a17cef9dedf98c4ebae2fb0c48c8f75952c877cbc2efe09f329ef16c2783d88a", + "sha256:fcd3a130f575fa36403d38774af1c64a4fbfbca09215f0589d2372b5119697cb" ], "markers": "python_version >= '3.10'", - "version": "==2.54.0" + "version": "==2.55.0" }, "google-genai": { "hashes": [ - "sha256:37a9b3cb127d763e7f4ca47452ae3562c87728773bd1b149f7b559c239da2bc1", - "sha256:4da0a223a100f4b37f609a68b835e3326ab0fa313314dc0fd9d34e76ee293844" + "sha256:2a79e2b08e8439f5f25c2b42f98e3f3e8ea4be9c9265f5d7321580dbaf2764f4", + "sha256:a8a10e9113f460cc668c1d9deeb62ba393ad1ba704bf3166d5a0f32a434f9415" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==2.8.0" + "version": "==2.9.0" }, "googleapis-common-protos": { "hashes": [ @@ -732,88 +732,88 @@ }, "greenlet": { "hashes": [ - "sha256:001775efe7b8e758861294c7a27c28af87f3f3f1c20468a2bc618c45b346c061", - "sha256:00929c98ec525fd9bf075875d8c5f6a983a90906cdf78a66e6de2d8e466c2a19", - "sha256:017a544f0385d441e88714160d089d6900ef46c9eff9d99b6715a5ef2d127747", - "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", - "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", - "sha256:10a9a1c0bfbc93d41156ffcb90c75fbc05544054faf15dcc1fdf9765f8b607f0", - "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", - "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", - "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", - "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", - "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", - "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", - "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", - "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", - "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", - "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", - "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", - "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", - "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", - "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", - "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", - "sha256:540dae7b956209af4d70a3be35927b4055f617763771e5e84a5255bea934d2f5", - "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", - "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", - "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", - "sha256:6c09df69dc1712d131332054a858a3e5cca400967fa3a672e2324fbb0971448c", - "sha256:6ebeb75c81211f5c702576cf81f315e77e23cfdb2c7c6fcb9dd143e6de35c360", - "sha256:73f78f9b9f0a5c06e5c946ba1e8e36f5114923b6be109ee618c54f079c3ea14f", - "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", - "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", - "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", - "sha256:7eacb17a9d41538a2bc4912eba5ef13823c83cb69e4d141d0813debe7163187f", - "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", - "sha256:80eb4b04dadc4e67df3fae179a32c4706a3f495bc7f22fc8a81115d5f5512188", - "sha256:88e300d136eac057b2397aa1cfd7328b4c87c7eb66a09c7bc6a1292234db474e", - "sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249", - "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", - "sha256:8a271fcd66c74615cda6a964fda3f304267a12e50a084472218a39bb0376f563", - "sha256:8d8a23250ea3ec7b36de8fa4b541e9e2db3ee82915cc060ab0631609ad8b28de", - "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", - "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", - "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", - "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", - "sha256:9d59e840387076a51016777a9328b3f2c427c6f9208a6e958bad251be50a648d", - "sha256:a0cbed8bb44e23c5b199f888f4e4ce096b45ad9f25ff74a7ad0213875e936bb2", - "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", - "sha256:a203a8bd0acb0701653d3bbb26e404854a68674139ed5cbb778830f42b09bb33", - "sha256:a4764e0bfc6a4d114c865b32520805c16a990ef5f286a514413b05d5ecd6a23d", - "sha256:a57b0d05a0448eed231d59c0ceb287dde984551e54cbc51ac2d4865712838e9c", - "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", - "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", - "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", - "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", - "sha256:b0703c2cef53e01baec47f7a3868009913ad71ec678bbecb42a6f40895e4ce62", - "sha256:b9152fca4a6466e114aaec745ae61cba739903a109754a9d4e1262f01e9259b1", - "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", - "sha256:c3d35f87c7253b715d13d679e0783d845910144f282cb939fe1ba4ac8616269c", - "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", - "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", - "sha256:cc6ab7e555c8a112ad3a76e368e86e12a2754bcae1652a5602e133ec7b635523", - "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", - "sha256:d0932b81d72f552ded9d810d00021b64d89f2195a91ce115b893f943b7a4ab3c", - "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", - "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", - "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", - "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", - "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", - "sha256:ded7b068c7c31c1a8657d4fd42d886b3e051ae29f88b80c5ff9d502257b0f071", - "sha256:e5cc9606aa5f4e0bde0d3bd502b44f743864c3ffa5cfa1011b1e30f5aa02366f", - "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", - "sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee", - "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", - "sha256:ea37d5a157eb9493820d3792ac4ece28619a394391d2b9f2f78057d396ff0f0f", - "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", - "sha256:ed8cdb691169715a9a492844a83246f090182247d1a5031dc78a403f68ba1e97", - "sha256:ef08c1567c78074b22d1a200183d52d04a14df447bf70bcbb6a3507a48e776fc", - "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", - "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", - "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed" + "sha256:01e32e9d2b1714a2b06184cb3071ff2a2fd9bc7d065e39198ab21f7253dad421", + "sha256:0488ca77c94da5e09d1d9958f98b58cebba1b8fd9664c24898499133de927574", + "sha256:049827baab63dda8ab8ec5a6d07fc6eb0f418319cfc757fc8737a605e99ca1ad", + "sha256:0629377725977252159de1ebd3c6e49c170a63856e585446797bb3d66d4d9c34", + "sha256:09201fa698768db245920b00fdc86ee3e73540f01ca6db162be9632642e1a473", + "sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32", + "sha256:120b77c2a18ebf629c3a7886f68c6d01e065654844ad468f15bb93ace66f2094", + "sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4", + "sha256:1724499fc08388208408681c53c5062e9803c334e5a0bdaeb616228ba882aac8", + "sha256:1adc23c50f22b0f5979521909a8360ab4a3d3bef8b641ce633a04cf1b1c967ea", + "sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb", + "sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8", + "sha256:1f052fff492c52fdfa99bd3b3c1389a53de37dae76a0562741417f0d018f02b3", + "sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682", + "sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163", + "sha256:2c3b3311af72b3d3b03cc0f1ffd11f072e834be5d0444105cf715fc44434e39c", + "sha256:2c6d6bfa4fdd7c39a0dbf112cdf28edbd19c517c810eefb6e4e71b0d55933a4c", + "sha256:2debcd0ef9455b7d4879589903efc8e497d4b8fb8c0ae772309e44d1ca5e957f", + "sha256:2ee6288f1933d698b4f098127ed17bda2910a75d2807915bd16294a972055d6c", + "sha256:30252d191d6959df1d040b559a38fc017139606c5ecc2ad00416557c0355d742", + "sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32", + "sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de", + "sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904", + "sha256:3c417cd6c593bbbef6f7aa31a79f37d3db7d18832fc56b694a2150130bde784e", + "sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a", + "sha256:423167363c510a75b649f5cd58d873c29498ea03598b9e4b1c3b73e0f899f3d5", + "sha256:4e554809538bd4867f24421b43abde170f9c9b8192149b30df5e164bcac6124f", + "sha256:537c5c4f30395020bb9f48f53146070e3b997c3c75da14011ab732aaa19ce3ef", + "sha256:561dd919c02236a613fbf226791cbd77ee5002cbd5cb7e838869aa3ac7a71e16", + "sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8", + "sha256:5930d3946ecae99fa7fc0e3f3ae515426ad85058ebd9bfc6c00cca8016e6206b", + "sha256:5eba55076d79e8a5176e6925295cfb901ebc95dae493342ede22230f75d8bee2", + "sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9", + "sha256:6d9e19257794e28821c9ebd5e23f86d7c267cd9d390089374f068d2049f949e3", + "sha256:6e9e49d732ee92a189bb7035e293029244aeba648297a9b856dc733d17ca7f0d", + "sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f", + "sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39", + "sha256:711028c953cd6ce5dc01bbb5a1747e3ad6bd8b2f7ded73778bb936e8dab9e3b6", + "sha256:76dae33e97b52743a19210931ee3e78a88fe1438bc2fc4ee5e7512d289bfad4f", + "sha256:7a7bfc200be40d04961d7e80e8337d726c0c1a50777e588123c3ed8ba731dcb9", + "sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce", + "sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00", + "sha256:87359c23eb4e8f1b16da68faad29bf5aeb80e3628d7d8e4aa2e41c36879ddedd", + "sha256:89da99ee8345b458ea2f16831dad31c88ddcdec454b48704d569a0b8fb28f146", + "sha256:9558cae989faeab6fbb425cd98a0cfa4190a47fba6443973fbee0a1eb0b0b6c3", + "sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c", + "sha256:9dc23f0e5ad76415457212a4b947d22ebe4dc80baf02adf7dd5647a90f38bb4e", + "sha256:9df9daae96848508450011d0d86ed7c95f8829a354ce438284a77b24896fd1f8", + "sha256:9e194b996aa1b89d933cfe136e5eb39b22a8b72ba59d376ef39a55bca4dbf47f", + "sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb", + "sha256:a1759fa4f14c398508cf20dc8037de55cc23ae8bd14c185c2718257837195ca5", + "sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3", + "sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317", + "sha256:a2ddf9eddc617681108dd071b3feabf3f4a4cd64846254aec4d4ceda098b639a", + "sha256:a3f76a94e2d6e1fee8f302265679d8cc47d71a203936dd03c6e2ace0f9cfd46d", + "sha256:a850f6224088ef7dcc70f1a545cb6b3d119c35d6dca63b925b9f35da0635cdad", + "sha256:a9476cbead736dc48ce89e3cd97acff95ecc48cbf21273603a438f9870c4a014", + "sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0", + "sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1", + "sha256:b4cad42662c796334c2d24607c411e3ed82481c1fb4e1e8ec3a5a8416060092e", + "sha256:b9318cdeb9abdbfdd8bc8464ee4a06dffde2c7846e1def138365a6240ab2c9a5", + "sha256:bc18b8d33e6976804b9b792fe11cb3b1fee8b646e8a9e20bf521a429ddf73520", + "sha256:bf493b3c1c0a2324c49b0472e2280ba4665f3510d8115f6f807759a6163b15f7", + "sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a", + "sha256:c1b906220d83c140361cdd12eef970fb5881a168b98ee58a43786426173da14c", + "sha256:c1c1e5ad80f1f38ea479b83b39dccb20874cfe9ad5e52f87225fa294ba4d39a1", + "sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73", + "sha256:ca92411942154023c65851e6077d8ca0d00f19de5fa80bb2c6f196ff6c920ba9", + "sha256:d7792398872f89466c6671d5d193537eff163ecf7fac78d82e6ddc25017fb4f5", + "sha256:db548d5ab6c2a8ead82c013f875090d79b5d7d2b67fc513934ce6cf66492ad7f", + "sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95", + "sha256:e063263ce9047878480d7e536012fc8b7c8e1922989eb5f03b9ab998a2ee7b7e", + "sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c", + "sha256:e976f9f6941f57d87a194c91868622c8b22a142a741d2fde31655c319133ade6", + "sha256:f41feb9f2b59e2e61ac9bea4e344ddd9396bf3cacb2583f73a3595ed7df6f8e7", + "sha256:f4d67c1684db3f9782c37ee4bade3f86f5a23a8fcf3f8359224106018ca40728", + "sha256:f9bbd6216c45a563c2a61e478e038b439d9f248bde44f775ea37d339da643af4", + "sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74", + "sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c" ], "markers": "python_version >= '3.10'", - "version": "==3.5.1" + "version": "==3.5.2" }, "grpcio": { "hashes": [ @@ -1073,12 +1073,12 @@ }, "langchain": { "hashes": [ - "sha256:4af49ad1095799e4408b489fb79d4b8b49292453618b202d8a697fca59bb6871", - "sha256:9b14ef0db9ef314299ded858b22ca2a40b8f1b05c8c9cb6b82d53a53075fef00" + "sha256:5da67f21aa56119744ad51b3e46ffac570c88f4fae0876e3b1c6a1c4bc0e344e", + "sha256:fd6ac9da86c479e4ff376e772d9e17a9232bd3113e9f2ddcb70cdc4bf7afc119" ], "index": "pypi", "markers": "python_full_version >= '3.10.0' and python_full_version < '4.0.0'", - "version": "==1.3.9" + "version": "==1.3.10" }, "langchain-anthropic": { "hashes": [ @@ -1108,12 +1108,12 @@ }, "langchain-core": { "hashes": [ - "sha256:7a825d77de0a3f39adbd9d09612a75e85527e14a52c1601089bcc062972d9f2b", - "sha256:bcadd51951140ecdcba98311dbd931ba5de02a5ba8a2288dad5069c1eea2a13d" + "sha256:5bf1f8411077c904182ad8f975943d36adcbf579c4e017b3a118b719229ebf9a", + "sha256:d84c28b05e3ba8d4271d0827aad5b592ccdaaf986e76768c23503f0a2045e8aa" ], "index": "pypi", "markers": "python_full_version >= '3.10.0' and python_full_version < '4.0.0'", - "version": "==1.4.7" + "version": "==1.4.8" }, "langchain-google-genai": { "hashes": [ @@ -1144,11 +1144,11 @@ }, "langchain-protocol": { "hashes": [ - "sha256:982a08fe152586ed10d4ff3d538c2e0b5766e5f307cdea325e10be3f2c17cae6", - "sha256:e7cbe58c205df4b4fd87dc6d5bb23f10e13b236d0e2e1b0b9d05bc2b648f3eea" + "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", + "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6" ], "markers": "python_full_version >= '3.10.0' and python_full_version < '4.0.0'", - "version": "==0.0.17" + "version": "==0.0.18" }, "langchain-text-splitters": { "hashes": [ @@ -1169,11 +1169,11 @@ }, "langgraph": { "hashes": [ - "sha256:09a3bdec6fdb3228623fc78b6f69a1400d383f66348d0b04d0efb692022cc6ef", - "sha256:9286bb5def82fc865959c14378fe473518dc097d586225f622f029637a2a4bb9" + "sha256:1cf94d3ca124f84f77ce408fa1b06c3dee680a8aafffe364a8fd5d7d03eb8695", + "sha256:f9b45a34f13930c94d96cdb76277447ad2cc70ec2d18cd2764d7fdadb36cdc1b" ], "markers": "python_version >= '3.10'", - "version": "==1.2.5" + "version": "==1.2.6" }, "langgraph-checkpoint": { "hashes": [ @@ -1201,11 +1201,11 @@ }, "langsmith": { "hashes": [ - "sha256:72e57c79eef82ddbbe22c5edb8bdf79eb557e65c3a1172ddfa05a724555c8294", - "sha256:9cdf26495814c9ae38998cee7895cdba267b0b3566b598e407c0bb7eefb5aaf9" + "sha256:32dde9c0e67e053e0fb738921fc8ced768af7b8fa83d7a0e3fd63597cf8776dd", + "sha256:3940183349993faef48e6c7d08e4822ee9cefd906b362d0e3c2d650314d2f282" ], "markers": "python_version >= '3.10'", - "version": "==0.8.15" + "version": "==0.8.18" }, "lxml": { "hashes": [ @@ -1682,11 +1682,11 @@ }, "openai": { "hashes": [ - "sha256:23d617a0432457ad844973bee8f540be9da90894f7c5686852d2d365da058f57", - "sha256:a939565f350cb7443cb843b801b88c716ac8024b492fb94ca269d5f6b1bbefd6" + "sha256:65a670b54fadf2268c9e1330133373c963eb779ee969e5cbad419ec2c21dce97", + "sha256:e74d238200a26868977002190fb6631613480a93dfe0c9c982e77021ed60a017" ], "markers": "python_version >= '3.9'", - "version": "==2.41.1" + "version": "==2.43.0" }, "opentelemetry-api": { "hashes": [ @@ -2343,11 +2343,11 @@ }, "pydantic-settings": { "hashes": [ - "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", - "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa" + "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", + "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f" ], "markers": "python_version >= '3.10'", - "version": "==2.14.1" + "version": "==2.14.2" }, "pydub": { "hashes": [ @@ -2664,118 +2664,104 @@ }, "resvg-py": { "hashes": [ - "sha256:040d70ae71cff3b0debe2a9d8d1ed4c39232dd0d43ea7f5801a6b56a35243f49", - "sha256:092fd27b5ee7d480a59c6c3c15a4a6ab551ab85909c41dd66f9ff1741e9f593e", - "sha256:0986d9150b603e534ba14e42ebb13952b18eb718491e92bf1e14e483c648596c", - "sha256:0f565ad3fe1bcf35b3bd1ba962dd3254cf2c7a30046c10160d71a098b7bb43af", - "sha256:10ac52b9f651d20d02952d1d08b8e26a1e487cae0a90de2ef4d3071cdb057a0a", - "sha256:1276bb7d4a0c73c1a6b9c08bae54d85affc934b762f5843d892a516d2c023d97", - "sha256:1866e2090eb0e493ca2732d37404f027ce262083322fc5a8d0d175dd20230e56", - "sha256:1a28a96aecde3253e086320fd04a7c59e6abfa9d0af28f0c9b0f867a82245bc3", - "sha256:1d13be473fcbe71a29e18263054b2d13f470b82a30aa24469d7dafe522058505", - "sha256:1d8b9973108f194d7ad752020e706dcccbb34e8f784024311fa1c0c01659b5c4", - "sha256:20e986c963f1bda4c1c42a01f89fa7653550f87ff8922da49773d07265a31f79", - "sha256:22a268e8be3b23279a8d9461a24d432587469d4811d3a142fbef310463255765", - "sha256:2f49de6c24a9faa773dc11f7f1b319b22ff4485fcf3d8b13c46c07d21c53bd49", - "sha256:2f5819f043811b81d1518f712ebafcd791abbaa479888d54b29cc764976868e6", - "sha256:3006f46efbb29a6c62ed3f22017814ea4cf99a190c9b69ea63287aefb12c8c66", - "sha256:30e4e92062c93e782506e54d39a96116a1eaaff5b0e08ff94c5f52452b061c75", - "sha256:31c562658dbb6f504c45a49120f0615c070cd28a8d9e1ca3704803e2e01d952e", - "sha256:332fbe99f5944db2ce0aefeaa29c8abd7e39a7179f8fde9c2475240aa7b14596", - "sha256:350eabcb016ef81cce93a1e3f5d9dae0f2f68a2760ba0925ab63c3f48c647c47", - "sha256:356a8522cdbafa7ef9a0546450489e02270a5a723ec50b4c5818b4f2b8f02893", - "sha256:356eed35c4eb9d21933e60ee75f798c0459702a97ed0a210404d7f7b9f032d3f", - "sha256:388b23701e40be279afe4e60da068fe3454dd43167584461c320a9f55a83a2bd", - "sha256:4004e7d1f3cab2000646400fc131388e2a86b092c3814288facd21bde54c7938", - "sha256:415509d213eaba339b4118a6e36cca7b6ee760b21703cdfdc8d9c934e30bb99b", - "sha256:42f7663406f4892f2df4a73c2546e1bc809b43fb5ac5d6665fe5d5f4600bb770", - "sha256:4772f64aceaf6777ef2a10a69f17b4a8db830d01f3bf092b2a094fe728131f83", - "sha256:4c40cf2109a15c83463038439fe70ae4b78025745be93a44bdb7a3bd43887981", - "sha256:4f05950f54ed4c33ed6d82668e75c7e4759a8f5e647f9c4b0db2bf4414a96962", - "sha256:4fdd946a0fbcc00423d2f7de3c95f84b5573d50011e515a9f1edf1518ae522e1", - "sha256:50163425cbe043ced2556817f2347aed01fba024cd7fe9dd6b7f49344f79eb54", - "sha256:508a4ed2d51e57904c46051835ed14651285d34be88542a048b977becadd2f32", - "sha256:5207bf2c246535ec9bdae28ba6cb92854f9511509ef1cb01733b1e9740198da8", - "sha256:57840840a1728003a2c7cde85f66241a6b9197746c9a02c374aa533ef2292ce3", - "sha256:5b9e324236898a3610907b2e7060c599fb6438a5e0e5c72f8314980eebf48be8", - "sha256:5c53cebc9baba81eabc5c8102048de5190b18252ae5c73ed2501e27ad2fda2fe", - "sha256:659df07ddde57b6755921069117dcd2bedcbc9de4e27a142a928dda98b562f59", - "sha256:65aa10f815e06ed063d93d949da481c73492fb6409260fa1e836ba523f9099e0", - "sha256:65b9ed1f42fde56211c4b1b47389beebd67861a971d65eba5e67e52b3e80f03d", - "sha256:65e9a6f6118be7a20fb57182da3f29b8c06ea329810e6041e7c7b245a0b6b7c1", - "sha256:66874a19209a905b278320e91739d8419f85870f9dda4fdaaf9bbd8dad83610f", - "sha256:6afef0dbcd30ad6a4e1be35fbb2c6706286fec70508e0246c9bdeea580ef4555", - "sha256:6b04e5d14977cef633b0a00d335f6727c9c27b024bf4a853d90277906ae2c390", - "sha256:6ed8bdb6a8d7539994511359ec657f00cd2c5f503c237cb24b9c5b309f64c9be", - "sha256:728507db685d949bf0bae7a255d36f645504b8891219e679511589c3d84cd576", - "sha256:7406be0435f91e33fefe4f6e56dd13a7d6bf178ba0ab7d60cd112404ebbac99c", - "sha256:7509b865a20223330b8b3517f6aec4613088ccd7155cd163105361d04120985e", - "sha256:7722c2652799b398450c5a926f4004220cc2c9b029ab97f1c9eed2cecbc1e735", - "sha256:7c459cdb4d1d0a5d9dbfee711f84fbf4551fba08754d21e085b3bd356f851fe1", - "sha256:7ce9e9ce3fdad8cb749688d12ecb5636d3acac9406e19517c46911ecb510eec6", - "sha256:7eefcbb18b1c86703eb3577cdcc5f6edb94c76dacb340a07b5a052e16047333a", - "sha256:7f19d15377b2d53b4a3fa6eeeee54774fd7a5642ea7e0ff91142ec295f988542", - "sha256:81b54cd02f11d83c82e278bdce7f71d674c6674ee60805c9764956b0492e7762", - "sha256:8398652c548e71c2bad914205be2735109e714ed0142a0c9ae3df1984fa3fb00", - "sha256:854a4bb16b8c61177ab543f2711c7b6a0d1f84fd3e8644aaf5e01587fe4c2750", - "sha256:86af2d12d74d949c9e38dccc7264014e503770eb27c678140a9d411a0c43ddc7", - "sha256:877a7b9056395c73644d8987ab93c99996aa3283843b49ad2a4443a3b44cf969", - "sha256:87e139d098ee82dc55092fc04da7e2a31cb100d63c7ea991bf8e679caf8b4b41", - "sha256:8c84a1053d319a0494fdf78432545460379adae6639aad6fb5f1f2e829940bc0", - "sha256:900aa5d6b04ce503e4114860aee24a10862d7bd2b164a5a7167af7a2769f08b6", - "sha256:91a7dd1bc684e947b8f11d5c2daf73b6e1a48d47e2f0be6e0f017797ae0dd0d3", - "sha256:94a1ae07976d963ade456f88ea63098fdc29bd1bea0df04c4d228588bc8fc621", - "sha256:966b0a12058c3f6f5956a7fdb52869f6cda4462868fb83f9d8eb412ffcc3b4e0", - "sha256:9750881a92863236e49776b97ccec8551835eca1a63fcb59baba97d30a2c3daa", - "sha256:985d08609be97976d9516788f26596a6a64ba3f786e13f3e2a90fb6f660a0b5f", - "sha256:991bccafe6487010f9b635e0c01620550d49f504020dbc4a5a75702efdfc8294", - "sha256:9cc5361e73e4f1ff7de8c65827d8e0de9577b9bab08684341ccf2fdeb1893426", - "sha256:9f59db5ef2cd7167b4ee389f528b159b9ce199dd9fa0e60c0227c3f24d00f401", - "sha256:a19034354e4af48993b9397123049344ab4766c2b44c72d5fdc0ee49c27023cc", - "sha256:a2010b59519128bc80055cf358e24c8c2b53550d7dd2a44742e9c2bc44e5e398", - "sha256:a2d4abb7ac0a4bba63f298a7203dc661b9344ff34771c997a0d0743d85c0cd35", - "sha256:a53346138595a2d97bb71597f4723cbba53db98372b1b080218076a9c44d1e3c", - "sha256:a79243683535fc4bf5d6617979109e6ad9ae555ebefb993622e1ddc81192a9fe", - "sha256:aa1dfb6e1ac37fefa55e9767459680069f58297902375106e201fcaedd481dcc", - "sha256:ab559b5845ed45aabd474710bff2d7a73a6bc6026b78fb73f958408fb373f5cd", - "sha256:acf2d47d0d8a7fea1314ad8e60f6f095ca352e9c28cb79074cd92ec72d89e8a8", - "sha256:ad547571eeeaaf45b6cef7aa49de575124533d1ab5f6275468f1a80c21d72d40", - "sha256:b1c40eb2ce447e8280c3eb644efb312c1307b481936f39c2f7281bac9a879216", - "sha256:b44b2b41bca6437976226a11acd41cb7b77fa06ed007d0953648d76d0ba6f4db", - "sha256:b65694c1c9b9f392255bdd61dd5b0d6a965bde923444957f6e0db6b287c95469", - "sha256:b68f7b11aac933952449925ff1fa381503a77260ffe12f902125f4581477a541", - "sha256:b9f7862cefd6f607690152acbfc98ae29ca82461e383fb0ad7fd5accc30034e5", - "sha256:ba4d141aa98ab05b8a1b0c8837acba0b1cb98429571db805c4d93855dedc9262", - "sha256:bcfa9e39c600420953bb3f4cf859eda0fc8cc070674b798fb1bcb41835e93852", - "sha256:bea4760b2b5ad416c1d5cb116d57118d37465df6bd6dce633e74b4dec7ac884e", - "sha256:becdb22ff66023b0f8f4f378a056e40c7d5a02c1ca81bd796591dd3ddffd756c", - "sha256:c1ae75c33fa328119a237f368df92209f41b7602c57ab8d2d67b899ea7a1dd97", - "sha256:c255b35424acdf3c66eda7e1384b780f6a90cb17c96bb6faadcd21fe93842542", - "sha256:c637b7dbd3fa5737b748ec0a9379b6be744698c51ce8c8dfab97a64e8cbd1964", - "sha256:cb74f863ac9d0997effbbd9e9c27b3c6df40f167f6a1cc5c1b982a8983e0fb36", - "sha256:cc776c4eddbee4e39f0db89ad14224a8d7206f3d0378f7b9bc1fbd2a4d1f78cd", - "sha256:ce4dd28bd82e2083a0efa8b9a26bb8a47c0a4d1ca894021a17f4d15963d9ec93", - "sha256:cf12829fcd960091293c7f9442e6a6e49eaacff8be9dc9f8ef6efbbd5f218b4e", - "sha256:cfebf7c2153bec45d8dc536b296f87570411fd85fbeed9cc2955d241ba17fdb8", - "sha256:d34bdf6bcf1cb6c431dfb202b4959626a9f8471972fa51d1dbdd2f0afc81eb75", - "sha256:d43d407ee684a302439ecc726e53000bb71fa8e22d2fa92c504383f58b056be5", - "sha256:da3326876bdfe93407c0afd9e6d61b8f78e4357cbb922e28aa4367bf10add2bc", - "sha256:da44618229009bf2e3f16a7c6283dba75b3ea753269bb57859eedd59c18959b3", - "sha256:ddda286dc5a1e9a2a4ae35a5b700b81eacf400c560c3032d59a40a45f5f4fbfb", - "sha256:deda8a5ca6a3d9d74c201c95727a9e53491171c56f747c9e938d9e72295257d8", - "sha256:e402791b3f83d086a9a96c28f34e7970d1a0047b5a51d65244052a4b17c80d74", - "sha256:eeba3965a0e102de8909a9370ec56e605a9006cc9ce3293edd26c3b3b782c6ba", - "sha256:f0d7844d80af88444e2c25398937dcb296427c1b3d5178cd7911db62210c87f3", - "sha256:f3dac8d29f09a86c46ac8cee8259276cf366e5031d43ab82f27b72d032633968", - "sha256:f812a79b07a94e7ab0f14a7ab7ba7d15cb97dc2f769efc3d66b707271818944a", - "sha256:f8a41da73f9fd456a3443f39967b087843b34e8b6e9f87e2989b09c58a7c796e", - "sha256:fc21bdd26a4a791c93192566f65dc1e521519973f4e92e2f0147aa9ddb9f3fec", - "sha256:fdda7269f2260f3ce6514b7a71f8cd13f957304f2d4e40a7dfbede0cccb357f5", - "sha256:fe2986e3096d8b35470141b4d29a53f3f46015d49f9f82b6104b8fc79bf9d819" + "sha256:03efcaa6929b6ed5abcfb4984399f3a3c35478ba96965b7f9757c642b22dbe70", + "sha256:17856adeefcf221c632c694f80d2648d207b0f73654caa00218d48955c3b0622", + "sha256:18524420b5a96336a147703875d2d6a74b974776f7a3081ca416c7413b90c45b", + "sha256:1a858234af1c42ff9f75d449b0b2f20ac819c1075bb9221c2c3170840cdb5685", + "sha256:1d1d615d1f39371ca1e6f43c2799ae7de9d8dfafd18fd78b7d04b100f987f9ec", + "sha256:20abc5a85e47b859613806e4feacc62409b513fb9d00831c4c24a449beee8840", + "sha256:213f37b2d6f79f7fb0c4311b75483c29c027ce31c7edf545965aea0e57da7497", + "sha256:2524fc51eb2d4cb39d5850864416d3fca152979be2490a388083e71f45ad3370", + "sha256:28c401eba4e7b43b51ae5c16aa0b684f92dade7b8ca7be0e5141b81e0bfed356", + "sha256:28fc4e4e53bce5999f496b8491fe3f637f969792a0a1da48fe489f943cf0101f", + "sha256:293e532a31a99231024290949249592bcf3823c75b23985684265f98281537ba", + "sha256:2ab7fc40a6c6a2f050c79ac2e3568a98e30f1650ea5513893ed6615f73355fda", + "sha256:2d1a8b87618e1dcaf86f21d71d6b5573a7bbf666509df47603aa71abcb0b7c85", + "sha256:2f3a199a51221fee9bd904c3a0b5c60c842e0197b21f0e3198d3de063a7a284a", + "sha256:3246c1dc7fbf5ed9a71cc730238d2089efeb9ea7c3e774e040d0ec55c5571a96", + "sha256:341a5dd87fc2089e1d6b227d6e0730f7686192ec2fd9d7543a897b0ccfef3674", + "sha256:34485a62e3dc1e502d91855154d57cb54e6dcaf28a558e33205d77892585360d", + "sha256:345e6e65b6f1d3b55c088e0aff1ca6ad40b85c43140811c444731bb0dfe7433d", + "sha256:36b12285554208096e31ea55d4135cd0f4a6141761e52b8e65252bb3eda8e555", + "sha256:37df4823b84e9b3248c17714df8f1c35420a5f5f6e04f22045ca9c09e8c94628", + "sha256:3936060b7050b9174087756c7db3317258759770cf7a68aa8c79f34e6639f66d", + "sha256:397ed997a2f7a737f5d78fe49ac41f45b7f2481e7001b3691a7a12f71267ed88", + "sha256:3d48bdfd3a75ef7de6d27fd0b50a43bd8dd7a66fa93e07f690a98a9767478bb2", + "sha256:431ebf3a667f1b02c4c6ae53d212969cc0e1c551ff1bffe29adeac6d7a9f9fb7", + "sha256:45427ef75e949f01a419d6f2a71ff37f14110d52a7364fe43f7248b88fd282da", + "sha256:47b2de290d80891742b800eec09076f4c299f468181cb6c81656ffd10c4d8267", + "sha256:4ae0760883318833f6d66d7b9702222c94167d5db60a7e3018298cb4d5fa0f26", + "sha256:4b30970735648dcc06327a8d614dce8f77a4c307d023d2c026e19bca3996325c", + "sha256:4fffccdb51359a867ca4aa602c15cda05ea9df63bf6d7860beb4b527850e615e", + "sha256:55fa6c84e675c95b0119e5d24af0f94b08b90942e532494290253f27df6b77f4", + "sha256:57194e058aacee5e5d7694c5dbb00cde56d9a760ef1433a7e7f2a8b22e2c7576", + "sha256:5722df8ac30b86fbd72e0ec4c63bf17bb16067ee006c232f93c37f42a846308e", + "sha256:5a14768012cf41232dd78ef8e571cf942f69ac8d3dd06923c408f9856f431c5a", + "sha256:5aaed9b203336f9f887b6724351f1c41898e91fdf5a02b1d427667da019b533a", + "sha256:5b533eb8b79e5ee36d59004397f78568f893da288e4f40e7eba907a4f8668cf4", + "sha256:5e6f8f78f40b7393960e4dddd937304878aa84d6f90effaf609e6308fde34731", + "sha256:626353e9bb20d26f8032d080a4efe3683bfdff739b38be574167077d552206f8", + "sha256:6515cb342b3cdac57c3c70ec4bdb05b8f26f10ab9a90d208ccdfd731a38749cd", + "sha256:66cf04a5708a6d31329aca4820079b8aaab56389e53adfff6f38ce33a54e11ce", + "sha256:6b92a38614550852b522ce20523efad16b9e7b2dad31ec61eb7f04897c0ee05c", + "sha256:6e6f3180cae3505706ce32eec1233a27fc7b9d5d304e20c04f79d07897c6a5f7", + "sha256:6ea28f39c363c89530c2aad01dc9eca94cfabc6bf1f899341921316aa506518c", + "sha256:7884f3e2d85e943fc9939ba6a639e373f45043ffec3b735df1fe9f02cb8eac91", + "sha256:78e9a1a5614f57f8f69a9c405a49eebeac9d81f008d807d33b09f9d67f52e664", + "sha256:79c1922c3db974f71cc8ee23bd6b18fdb24e13c74493cb1c9428c9f126f89ff8", + "sha256:81ad1f27feacab23317a4d639345e9c58dd2bbe8665b5010d52d774217dcf944", + "sha256:83e9d1a01152f3a2a2266486e3ff04b37b6d535902a8923d2fbe95b8b201ce76", + "sha256:847c32934bad0726265fd07979244e909da6c418a9ee02542ceb75d88fc2941d", + "sha256:864de609f4866580e033541273c7b61cb05ab815db7e52c089cea3c5aea60366", + "sha256:8c7bec5f079bf2debe16af59940fd01749f5d17700d15dd66cb4f4442cb80a44", + "sha256:903e098faad9862ec94fdbac019574cb2cc72381f1529f5aede133d7c954e695", + "sha256:9171465e12a1c8bae5f8d2e4929862b542d11d02d0db6e453ccd96c9e4d7edb1", + "sha256:9268ceb1629d34cfc6f244f0ad7a8fcd6376f9f4ce822c0c3c30df79de712fd7", + "sha256:a1a62745f93e21b2c46ad595d7dd70d36560506a1963d1829853cce4ca2f15ba", + "sha256:a4f185e73898995adceabc59a81c0da4765176d751b90be3aa73203990b36d28", + "sha256:aaac963aa895ee7a23e41209e6625605771f3807bee9a3556ea346517754cd0e", + "sha256:abe83cbb03fb990f9ad0af3e6a7981dd82d4175c5e64a41ebe6d520e0b5bd033", + "sha256:ad4ca89b858b8a4955f675d8c053308a92a35071e45881d0dec925073cb064a5", + "sha256:af10034ce6e6d164e4ad7b6aac50d62ec0a231cedff7d098e53e110e3d637617", + "sha256:afd3008c6ec8af1cea3130826d6a58cbb1c2c97039e501e287bcd7243de5a28a", + "sha256:b3f81dd5ffd49db66a0229b5ef3b82a63b0d2fbc3812fa0123ce1cdf0173fc5f", + "sha256:b4c78673fa0fd242dd7d9d802f475bdefb3cb57e5f504c2e2f3adca4c305d3ef", + "sha256:b5dc3f2f4550bd0ca3e03dd16d3a3af44a08b116a51012980eb4becd132e286d", + "sha256:b86beb3738eb7426169bb6f23b3aad060eb7939fbce8e27540a138c82dce281d", + "sha256:b8bed4f81f9d9e09a1c7f15d1f51c2797dea80722147c6c907ab56532f1593aa", + "sha256:ba8e52c504b220db092926c5c312a8233d95864bf0a5a3674be936f92a70c913", + "sha256:bd3d3c86ca7ab2ec809bd9e617ca59c6c37778464cebd77a542851c30d9dd018", + "sha256:bf5884dfc349510b874a107ee2eaec2ae6e7f6627bb6f754f9c3bb1b09a1bb11", + "sha256:c1ed821036d7e9009fb11b63ae7f41d158fe0a66ebd64ab6825fd3830ff1c2c2", + "sha256:c76c856fcdd1c15bd0134ae75d563d20f4d50ca961485c02f7f8aceb1d8aa385", + "sha256:c88a143eaf64316bdff1fafb93e622b276d3588d1d59ea4f5f03108e4d399dca", + "sha256:ca8a487f375d585d4dca7afedbfc17667731248b53d924d6471e73d42fc4f373", + "sha256:cb2dfec2e604477a2e831cb3d505b541590c1261df8a1e6d02745f80f405047c", + "sha256:cbe903a3be8538c06dedd7e992da0a71221a9ce938a7a6048731d2b152ee210d", + "sha256:d1f95e2e6cbb261d50d6e00fe731b6e24a271d827ef39f1f8987ec2352e077d2", + "sha256:d2b747d6f77fc985f7b7c5798b69ddbf0df8303d01b87899e34a2eba76e75e86", + "sha256:d3c6a565b368d6b6440ede6a8ed2ebab1f219586c09298cd27f50745fdef8e44", + "sha256:d431e352f0ebf006946725f86f3c7e7598b8401e83138ab1f21fe225f4a1cbb5", + "sha256:d6d38e48aedf6738097f8e59f4de83fcc48a0a783ff1457a3f614525944dd4e7", + "sha256:d95c89e55bef7093c9894a08a0f16482c4cc35ff023b5b053075675841e5c178", + "sha256:dbf698e09f2671a6d0f5757dbc15e5cb675b003ab5f11147e097553e8f2a1616", + "sha256:dd5b113ce8c8daf2ce37b547c6e1c359f8e9485ca1affa4cb4d6d482c0becede", + "sha256:dd7f32e0e3b3a7d5d8fc22384859303161e188eee614558b322f001848db2c73", + "sha256:de8191176a578492a6922caf33599a297322c78173e1b939b9464459fed635c7", + "sha256:e148630bea35d7897bc66116fb440b6ac826da10fad088092104e56343c12088", + "sha256:e4e6766406b8ca500c1f5c17d429aa3394d441d0381bdcf9777a86fb2a29df54", + "sha256:e5092efbad8e85104d2b014e9608ad3a5991e552acf57ff6339386af29831ac4", + "sha256:e6850ea01a74ba8ef922db312bc06393d46fff1411f60d1d6c7d02586914744e", + "sha256:f0dc7bb865abb7a3cb6092d63b9d0e06216c4b2d1615f5b9b4b488c120ed2326", + "sha256:f5bc650ce1fed0db08a719036d5b3e966c454921eb5ffdca19d48594e74ceabc", + "sha256:fa413cfd0854f7449cd37748ce642ce491d2e6e23f369d180b2861ed5d0e2ddf", + "sha256:fc0fd718a2db0b46a53d87537a7e3d9160e3074ad3a8b752e6d1b9a0af6edcb3", + "sha256:fe4ab3bd1e5e3ca3935d77f3fd1f681f5f7c555226cbc79a5e0153fea701f30e", + "sha256:fef023a7806b1749fb5ecbe27c4250ba7399f14721644b33cf0d3f5971c870b0" ], "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==0.3.2" + "markers": "python_version >= '3.10'", + "version": "==0.3.3" }, "rsa": { "hashes": [ @@ -2811,68 +2797,68 @@ }, "sqlalchemy": { "hashes": [ - "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064", - "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093", - "sha256:0a31c5963d58d3e3d11c5b97709e248305705de1fdf51ec3bf396674c5898b7e", - "sha256:0e104e196f457ec608eb8af736c5eb4c6bc58f481b546f485a7f9c628ee532be", - "sha256:0f5e4ac70e9e757f6b3e87c0491ff034442ecd8dfd36d041a50564c322dafc0e", - "sha256:0fe7822866f3a9fc5f3db21a290ce8961a53050115f05edf9402b6a5feb92a9f", - "sha256:0fec460e18cdbb4c7773531122ce9a27e96c6ca17af3933941d94da475ad2c86", - "sha256:110fdac56ace278949f00de805edacbd6141e382d992f9ba28238b3a0827a600", - "sha256:1208050441471d003b7c8cb4054fb084f185cf35ac3f0ea270803865bca9939a", - "sha256:13b85b20f9ab714a666df9d8e72e253ec33c16c7e1e375c877e5bf6367a3e917", - "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39", - "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a", - "sha256:1aa6e403663a9c43c8fef7ce4bdb4cf48bcd8d352e91deda2a99f963270bd508", - "sha256:1c5f858fe79c9f5d8fda065c06186356acb7f8df3cd52dbd5ee3f200e4b144f5", - "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e", - "sha256:23ae23d8b9d344d30d0a92f06d45825024a5790f1c1dd4cf452636a50d3e58cb", - "sha256:27b7062af702c61994e8806ad87e42d0a2c879e0a8e5c61c7f69d81dabe24fdf", - "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3", - "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f", - "sha256:2c1920cde9d741ba3dda9b1aa5acd8c23ea17780ccfb2252d01878d5d0d628d3", - "sha256:2dab927761d9108550f0cf8e66ff21af56f907a0ce0a689793db615e2b55f62c", - "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db", - "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70", - "sha256:3d10700bd519573f6ce5badbabbfe7f5baea84cdf370f2cbbfb4be28dfddbf1d", - "sha256:409a8121b917116b035bedc5e532ad470c74a2d279f6c302100985b6304e9f9e", - "sha256:47b71b933e7b4ebad407c8fdfd70d2c4f08b78b3238bb30eebdd6eb32ca51b89", - "sha256:4a8e8af330cbb3a1931d3d6c91b239fc2ef135f7dd471dfa34c575028e0b1fa8", - "sha256:51b637a84f9fa35ae1f9017e786cb142974a25305085e1b378b3647a67f65ad3", - "sha256:545eae198d37bcf837a10ede3684e2af32458d6f35c597c35c2de7502dc38fc4", - "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5", - "sha256:66e374271ecb7101273f57af1a62446a953d327eec4f8089147de57c591bbacc", - "sha256:68b154b08088b4ec32bb4d2958bfbb50e57549f91a4cd3e7f928e3553ed69031", - "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e", - "sha256:724f3dcbe53dd0151e3cb5e7ec4ba4c620bede579caacd16275dc35ce06e8615", - "sha256:7af6eeb84985bf840ba779018ff9424d61ff69b52e66b8789d3c8da7bf5341b2", - "sha256:7b1ddb7b5fc60dfa9df6a487f06a143c77def47c0351849da2bcea59b244a56c", - "sha256:7e36efdcc5493f8024ec873a4ee3855bfd2de0c5b19eba16f920e9d2a0d28622", - "sha256:83a9fce296b7e052316d8c6943237b31b9c00f58ca9c253f2d165df52637a293", - "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873", - "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8", - "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", - "sha256:9602c07b03e1449747ecb69f9998a7194a589124475788b370adce57c9e9a56e", - "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f", - "sha256:9d1af51558029a156a70986b7df88f042b3d158d7c8d8fb5072912d4b32d89c7", - "sha256:adc0fe7d38d8c8058f7421c25508fcbc74df38233a42aa8324409844122dce8f", - "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9", - "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d", - "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d", - "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52", - "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51", - "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0", - "sha256:e195687f1af431c9515416288373b323b6eb599f774409814e89e9d603a56e39", - "sha256:e6e814658818fd165e749e3d8490ef16cc7f379a118c37ada8b0589ffbaaac22", - "sha256:e8e1b0f6a4dcd9b4839e2320afb5df37a6981cbc20ff9c423ae11c5537bdbd21", - "sha256:ea1a8a2db4b2217d456c8d7a873bfc605f06fe3584d315264ea18c2a17585d0b", - "sha256:eefd9a03cc0047b14153872d228499d048bd7deaf926109c9ec25b15157b8e23", - "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086", - "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb" + "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", + "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1", + "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", + "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", + "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0", + "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", + "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", + "sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85", + "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", + "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", + "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba", + "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652", + "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", + "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", + "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84", + "sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46", + "sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7", + "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", + "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d", + "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", + "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", + "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd", + "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", + "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc", + "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", + "sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825", + "sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8", + "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", + "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", + "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", + "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a", + "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", + "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", + "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", + "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", + "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", + "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", + "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", + "sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0", + "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604", + "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265", + "sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904", + "sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a", + "sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64", + "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", + "sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032", + "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", + "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", + "sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2", + "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", + "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", + "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080", + "sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37", + "sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00", + "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86", + "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260", + "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", + "sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.0.50" + "version": "==2.0.51" }, "starlette": { "hashes": [ @@ -2955,11 +2941,11 @@ }, "tqdm": { "hashes": [ - "sha256:89c230e8dbc67c7615c142487111222f878c77427ea09549960f62389e258add", - "sha256:d4240441fb5353290b87d6a85968c9decc131a99b8c7faa28269d829de669ede" + "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", + "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03" ], - "markers": "python_version >= '3.7'", - "version": "==4.68.2" + "markers": "python_version >= '3.8'", + "version": "==4.68.3" }, "typing-extensions": { "hashes": [ @@ -2988,115 +2974,102 @@ }, "uuid-utils": { "hashes": [ - "sha256:04af9966ecd82b78eeba5725e29aa1e86fb8eb84b5443dd6a9935f9fadb6678e", - "sha256:0681d1bdb7956e0c6d581e7601dabcfb2b08c25d2a65189f4e9b102c94f5ff46", - "sha256:06fc7db470c37e5c1ab3fd2cd159697d6f8b279d7d23b5b96bd418b115f8caa9", - "sha256:0b3377ce388fd7bf8d231ec9d1d4f58c8e87888ddea93581f60ed6f878a4f722", - "sha256:10d21fddb086e69245c4f0f77c7b442471f3a242aa85f62954bff157baa1c5f2", - "sha256:10d3c5983f770b1b2847ad811c87a1c9e28f8155d1a27cc581abcd5abb386b64", - "sha256:12b6310beb38adc173ec5dc89e98812fd7e3d98f87f3ef01d2ea6ecb5d87994f", - "sha256:130f7452c1b87b7c16d0bdc1f32a1de531ae4cc4220ed4e691402bbcfc39e0a9", - "sha256:13a797e5e8f0dadc18351a5aa013815ddac25dce6864072a539d510910c95f71", - "sha256:16dc5c6e439f75b0456114e955983e2156c1f38887733e54d54205d3005223e4", - "sha256:1b0dcedf9266bf34a54d5cbe78648eaa627e02352f2a6923ed647530aea2f661", - "sha256:1baab8966f9e0097cbaf9cc01ad448b38e616e7b4968ca5e49cb53a74ad91a2f", - "sha256:1c2df42314b014c9d23330f92887e21d2fc72fde0beb170c7833cd2d22d845a1", - "sha256:1c3c5afaaa68b1d6393d653e9fc93a2fde9da1681da01f74b4593f41d31fb5f1", - "sha256:207c2a98ca8b065cc93378a3a59744efb88a68e9ecc2c3afefe43d59c864280a", - "sha256:228701ab6f188b6def24f2add6db64f0794adb1f06d0abacdcec40b0cda13cdf", - "sha256:22a17e93a371d850ffce8fcdbacc2239f890efe73aa3262b6170c1febc08afe1", - "sha256:24e6fa0d0ade7a9ad60a3c296022474983243df5b4e863babb4828a85ef2e52c", - "sha256:259bab73c241743d684dcc3507feb76f484d720545e4e4805582aeff8e19700b", - "sha256:26fe23ab60f05de4ad70aaa5b6a4c2a7bbd43055e3dd6f6b31efba0532ac9c71", - "sha256:27a071a899ba46a551d6524dbbc5a98b88be176d0f55ddf72cf71c005326ac10", - "sha256:2bb3444498e7b099499c8a607d7771377020fa55f7274e46f54106af19f752d7", - "sha256:2e2f369dd734050fe96ae4905c58779b09276d47d5e9a0e5cd33ec7982784341", - "sha256:38126b353527c5f001e4b24db9e62351eb768d0367febcd68100a4b39a035109", - "sha256:39453f1ebf4398fbeb71607f3437e2ac469c9e38b5921755c1e17ad0158a8907", - "sha256:3d86ca394e0ea21bdb53784eb99276d263b93d1586f56678cab1414b7ae1d0f3", - "sha256:3e1a1f57fe3631e164dad27b24aa81267810e20575f705af3b0fa734f3a21247", - "sha256:3ee392fe59808a731b7b6bf4d453fb6e833774921331cceae5f254d1e9c5b97d", - "sha256:41985e342a30e76366a8becc60bbdb07d72cd1b86ec657b1f31654e9fb1baada", - "sha256:41a67e546d9adf11c4e4cb5c8e81f000f8b1f000c17912ced089b499855719a5", - "sha256:420aa3ca403cedb73490b6ea3aeefeea7e0455f5ce60bbf856390ee872ae3306", - "sha256:426a8c9af90242d879706ccf29da56f0b0712e7739fb0bbe16baacabc75596e2", - "sha256:4a87a7433b355eadaa200f150da6bb5b87bb6de0adf260883b26cb637aba0410", - "sha256:4e35e9a986e86806a61288fac3afbb51317f2580929feefd1661891ffd7b8c24", - "sha256:50361aca5c2a770728a6343df85109fe57f89ac026827f34fe0153563cdc9ce7", - "sha256:5279bc7ab3c6683f1c67314695bee14d869015acbbc677bdb0015190fe753d16", - "sha256:52d2cc8c12a3466cd1727883e0746d8bad5dddd670369eb553ba17fdc3b565ca", - "sha256:542098f6cb6874aebeff98715f3ab7646fbe0f2ffb24509ca372828c68c4ed0e", - "sha256:57c3583b1f1c00a94f59726a5e2b988fa209221143919a1af5c2fc24e318fc98", - "sha256:57d85f48535dc541060f6b82f277cbcd12b78c04008ccc1039546cfcec027327", - "sha256:61a9c4c26ad12ac66fa4bfd0fdb8494724fe7a5b98a9fcd43e78e2b388663dbb", - "sha256:63bfdf00be51b6b3b79275d6767d034ea5c7a0caa067a35d72861284100cb60a", - "sha256:66a9c8cedf7695c28e700f6a66bde0809c3b2e0d8a70968be7bfd47c908952e5", - "sha256:680799a9ade01d69c53cb9d41392ced24919d4f600bfab5060b61fca37510097", - "sha256:6853b627983aa1b4fd95aa52d9e87136eb94a7b3b7de0fbb1db8a498d457eeec", - "sha256:6da070e75b0e2424728e6f8547647cce36c83f9a6101a08da4849a8ab2b58105", - "sha256:7207b25fe534bcf4d57e0110f90670e61c1c38b6f4598ba855af69ab428fc118", - "sha256:727fae3f0682191ec9c8ce1cd0f71e81b471a2e26b7c5fd66712fc0f11640aa0", - "sha256:733da81d51ea578862d8b9b754e8968b6da2be2b7840aee868917c23cae84015", - "sha256:73486b6aa3f755a6c97000f5ea67e7ac78d6df89bf22980789a1e943e24b74f0", - "sha256:7525bc59ac4579c32317d2493dd42cf134b9bb50cd0bc6a41dd9f77e4740dde6", - "sha256:7555f120a2282d1901c9a632c2398a614101af4fe3f7c8114aa0f1d8c1978855", - "sha256:756575d082ea4cb7d2f923d5b640c0efe7c82573aab49220c4e09b62d13737ff", - "sha256:79824850330e450c7b2fa933572e32192240060937426052fa3fc05134ed3faa", - "sha256:7f8cf49c05d58523a0f977cb7f11afc05791a0fa164d7303b8365a34750638e7", - "sha256:833bc4b3c3fc24be541f67b01b4a75b6b9942a9b7137395b4eb35435948bd6da", - "sha256:897e8ef0dc5e4ac0b17cf9cae84bb41e560d806280ec5b93db7475b504022105", - "sha256:9152bff801ec2ccf630df06d67389090a2c612dea87fbf9a887ab4b222929f6f", - "sha256:91db59bad97ed2b9d2c6ed25082fe9762b2c422e694fe06786b28cf4e776ac4c", - "sha256:924a8de04460e4cf65998ad0b6568084f7c51740ebd3254d07a0bcde35a84af6", - "sha256:9346ce6eb1fbd8b03a6b331d66016afcb4edcdff6eac708e21391600529a016a", - "sha256:948485c47d8569a8bf6e86f522a2599fa9134674bee9f483898e601e68c3caca", - "sha256:95b7f480010ea98a29ee809857a98aa923008c68129af1b39244adccff7377fb", - "sha256:98e2404713677070cee9a99a1f1e24afd496c18e833ee1b31a0587659452ff80", - "sha256:99f8420c3ed59f89a086782ac197e257f4b1debb4545dffa90cf5db23f96c892", - "sha256:9a250e111903c4368745fce5ac2aa607bd477c62d3307e45347338fdb64b38e0", - "sha256:a0fc6eb3fd821466fbab69cf356c6ec2b7327266bbbc740a2eb57c77c4bef965", - "sha256:a49b5a75497643479c919e2e537a4a36224ac3aaa0fada61b75d87024021ac3e", - "sha256:a4fd5c7936a876ba2606ba124603b559a5c2cea458c59b9c31677e6acc3c53cc", - "sha256:a632fead2a6505a8df3318d5e95503739b9aa1c518521cd93d83ce00699b78f8", - "sha256:a6d3ee32c57898d8415242b08d5dd086bc4f7bcbbb3fc102ef257f3d793eb294", - "sha256:a750d8aeb8ae880aa9a2529606bde0e994bcc7448730c953107f357a28e6102e", - "sha256:aa50261a83991dbb570a00573741455bd8f3249444f7329e5bdcd494799d1504", - "sha256:abfbf5e0c47fb31b37164a99515104e449a0bee36a071dc8b105457a2b35a5e6", - "sha256:b2e981b1258db444df4cf4bf4c79673570d081d48d35f22d0f86471e0ad795c5", - "sha256:b35706350cf9bd4813f1811bebe03cac09795a5a379f90cb3616171f4e9ffc9e", - "sha256:b42014536943c1a654ff107538c0f7dc39809d8d774ec8dafd19bec05006e568", - "sha256:b617a334bb01ef2ff8c22900f5a14125eb9063f602131494cc9dc59519beaa5b", - "sha256:b8a9a7b1065a12d40f2cc25b7d705ab34954cc57095034367bca39ebcf4a876b", - "sha256:baf79c8050eb784b252dd34807df73f61130fe8676b61231baccab62530f20ec", - "sha256:bbb92feb4db08cd76e27b4d3b1a82bfde708447317150c614eb9f761a43b387e", - "sha256:bece1a6f677ca36047442c465d8166643eed9818b9e43e0bf42d3cf73e92dcff", - "sha256:c5af79cde16a7600dfccb7d431aec0afd3088ff170b6a09887bf3f7ab3cc7c81", - "sha256:c8083284488b84ad178e74add64cfd1e74e8be5e30821e5acbc5019281c658b0", - "sha256:c97625e5edfda8b118160ce1e88756f92b1635775f836c168be7bf10928d97fa", - "sha256:c9f504efeb20ffd9571621658f7c8093c646d33150406d5742e49ff7cd861615", - "sha256:caac9c8b1d50e8fbddc76e93bfefbef472978eb45adbfdb6289d578816992953", - "sha256:ceef237cf8467fddbf6d8466cc1f6e2c04605ec919046ef5eba10a895b559fcf", - "sha256:d23fcaf37368a1647319187ef6f8b741bf079f033065899bc2d00a44b0a1214a", - "sha256:d34cf9681e8892fad2a63e393068e544505408748cd8bf0c3517d753a01528d4", - "sha256:d363017a3223de3a57eb6fca135df6ffcef7c534836bff2e71354dce7d10987c", - "sha256:d5ee0bbbd4ca3968422cd8308f0072520bc73dc760cb26c6fa75ca1aca14d210", - "sha256:d6902d4375dfba4c9902c736bb82d3c040417b67f7d0fa48910ddfdb1ac95de7", - "sha256:d716e5b35266400d2a2cd349697868179825f113c543e55c9d2ac304991f8d4f", - "sha256:d89927c47e1a55509e90b7f2fd3e7ff89908c77b61f8f0deda97a89d8854e0f8", - "sha256:dc0824a31898ef46a9d84d748c3abe27cdb615ac3773c53cc1f84fc8e66dc7c4", - "sha256:de8a365795a76f347f5622621c2bee543cffa0c70949f3ee093bdefc9d926dcc", - "sha256:e99f9a8b2420b228faba23a637e96efaf5c6a678b2e225870f24431c82707f50", - "sha256:ea3265f8e2b452a4870f3298cb1d183dc4e36a3682cbb264dbe46af31267e706", - "sha256:ed45fb8732d216426227096b55accbb87cba57febc86a044d90780b090eb99d0", - "sha256:efb5252d7c00d586077f10e169d6e6d0b0d0f806d8a085073f0d19b4737aef4e", - "sha256:f1614572fd9345cdc3dde3f40c237345719fabca1aa87d2d87b321d523cfa34d", - "sha256:f235ac5827d74ac630cc87f29278cdaa5d2f273613a6e05bbd96df7aa4170776", - "sha256:f44b65ae0c329843817d9c90e36a7a3c677b413bf407c99e67db874dac49dad3", - "sha256:f7ae4168e1ca0ae69d24207645a8b3cd2b641a0ad15058eda17d2c9898aa89d3", - "sha256:fbcac6e6710aa2e4bfbb81762758e01470dc56d5048ba4253acc77c9833568ff" + "sha256:01f81c71cf2185de0707e9d2f248e17025ba50af0acd3cbf51cd8aea96c2e0be", + "sha256:026b96b2f1e6b004579e030692d2f6568ccd0b29d40687213c31694abf570c78", + "sha256:0529b1ef0788f663e1211d221b59a38ec67f9b084f1ea5342ba84358b3d87e98", + "sha256:0aa2569908bdb21ccb216cd6bd06cb934351ee65ea7cd5e351e19f633a99b577", + "sha256:1a30b6a5790acb854e4b65fae7875e5d3c6f8076fa9c91dac43ff9e28380bc52", + "sha256:1ef8c561fdf88fec205e3d54037824cfe2addce16b509a8d2ecb69daa904cbb7", + "sha256:22446af2ae47d1054562b159bcd65714a022713e56697eef77cb5f291dc5ef13", + "sha256:27271b37fbc6812bb1542c4b8e22ee00223a6bf7f62b1f38d3bcf8e92f6d9acd", + "sha256:273679723e88544dd2de0564ab7f2fddfa2270faf05cabfdf63c275be67ec2a1", + "sha256:280d4f1f22dd2e79c1cc31ffc7fc26dc3534ffc114dedcdd29cc8489c5ce9c98", + "sha256:2cd4612085e6bbf6a00b9890779023ea97fe1ee8dd1758381022f7588a06e123", + "sha256:2e9ca7f5e215373cc9c147172170a0b1a4ab0dee9cc62fe446d9b075f31e3241", + "sha256:2ef60e0a91675cfd9850e8aefd0d899fe09c4afb572bbe0ac2de4f8848d7663d", + "sha256:3324bac95084e63e28553c92fac5a0394c636a76e03e50a7dab0c0bbddf87fa5", + "sha256:3aa5c2ebc843e85a078ec27c1ad677871c44065b3dd58748166783a3c454859f", + "sha256:3d4805c4739dd06d539f8f4fa94f5aaf26eca4b3ece1ef134d4ff904c6b08dcf", + "sha256:3d8257329f26905f009aed694bd3b17f334f43748b03134dc7bc99d6c5b4e371", + "sha256:3e3acb5e1451232381daea01645a98c69de4bb9ad88d77a1f7c1df4d83d54e62", + "sha256:41165aa4059e3b03605c1c8c48df6c887a16f8f6a1fc4cb2155360a61aad8666", + "sha256:4125bf6ed3ae443c05e140f8585d174b9d647295b12034d5ec94ae2ae38edefa", + "sha256:43cc72a92694d08ade8faadacf928857d9cceb84b449473246ae4e4f263d7d22", + "sha256:4942b26ad12c5187bac52b7fb4685040139ff0df9a19cde33e5025326f6180fc", + "sha256:503f020acc7dbeb39c47fa33cf2971cf5960fa11f8394513fac461762a90c556", + "sha256:5119bec75f56bd028d97472f72b1ed723a0d60b09a48017dc70a3cb1892ed081", + "sha256:511b5fde12d29c37a9badd399af62105bb2f4696aa10eb18be74e7b9ca84413a", + "sha256:585d3adf73afa60348bf2bd529491c640a692350e76d8ff3974455e273aadfe7", + "sha256:5dfc3e9e75139a84898771d31958ece6cdee8e8f127700aa8aa26a4f1a348d57", + "sha256:61454f2139424a6cff14eca7849c28b3350f261453b74075aa20fe99592dbb16", + "sha256:617955f4b3f649617c0388127d8a257202189d5cc3c720313f8b207df1cdb2a4", + "sha256:62b8841895eff1c0afbaf5f0050411667231160478c8ff9f411742abffd3b619", + "sha256:6c02f85f49c9c2abbf247a8622458c30232332a28711755aa191da5f38015af6", + "sha256:6f064dc54c6abecb09eb104d953bfb079f3c395e0d6b18899979f852d1083549", + "sha256:6fe3fb4bcecef69cacf3a11e182e204ce778998bd439152a173bdd2e9e8e9cfa", + "sha256:71192a59d473f3f638e2a238905046e2942006ad90ac5ec10d578e58ff9a08ce", + "sha256:725110434a1d482a639a9ac467a24f1cb531d84ab52e454a13fe145b10b42cae", + "sha256:72cfd9ff1e8a7c371a044687e77eb873721c4a9f4814e453439bfba595b84303", + "sha256:76632d2e16e26de777851ec07961ceaea14e65167d0603a0b17fb169fa9ca37b", + "sha256:79c5a3bd4301257b9a524efd16baf61ea65cd0d1b60b47d80f20b151fd65a09f", + "sha256:7f3cca9ca5e2c2dfd7b885f0d34c10b993a070d3593f3cdfef785195da36fb0f", + "sha256:8197870739a3094990743a80f075fa0b17beafd6c187e5f360e021d90a12a6d1", + "sha256:8323136bb02355c1b973492ab98b0722206dfdedfb148e4115c35fcdf3889bad", + "sha256:840b21e609a9b203eee06bdc73e18397154447a9814a8e78d9b68e5104d9802f", + "sha256:887efa34701d197239ec3b0e89993ee8c0cea1746483b606e54746ea81c966f4", + "sha256:89a627f74cb55aa508809592ab9149806649e4ee37f4bc91b60c7ec10929f0eb", + "sha256:8b8e325e61f918caf74ca540e3384b81e6e22aea782e57f615d15fc9773b96c8", + "sha256:90903ab7fcdfb0300390c15f5a68cb91f15139d9a1a93f134c783d7a973fa269", + "sha256:909e26fa2451c8db31b9ed1d3c8e4ecf513b6d1619db4205997fe99eb6b4ef4f", + "sha256:9282677ebf2ea5b437c20d16e75bcd7629bdc205018f95557b33b76868d8bb5b", + "sha256:945e1819dde4cce6828683ce11311977e73e6d46c6cc18e5fb9fcab2051b94bb", + "sha256:97ee6f5e803ea571f5f6da42efc97d8c5a13f121043680177f8470529b94e855", + "sha256:9ad2adeb941292fe02e1e5c70b80a5746c45b1b77594506c2a1421455d8384f9", + "sha256:9b4520521aa46a2582fe1829c535fe60b78999b89257db998df3816eb895bdf3", + "sha256:9bf8bfdffb22f620635580b17fd178272f30a9841b824b19b935c8db64bf09b6", + "sha256:9e97ab941660f781a8e45f15aba9ee01b40dbb96adb5c43617c1671a4604b25b", + "sha256:9fe600ab7d3d4eb56986e814042c917e728ac92cd8a41f099a6b59b84d8bf9e6", + "sha256:a59205fc15463dd0f978f14df14307737e3d4e8ef4aefa29a9d0fa766d84d16b", + "sha256:aab7cdf28a3e2859ca4f40a3e3bf53eb35895039c80d4d8d8c5e15b90346c55c", + "sha256:aac82500329ffaf2788dac36cf133e1e4e23b6d5e1118274ea6749c3b512f4f1", + "sha256:ae5fa2007fd26d26f7b09e76259d5ca99bec191616207ca929f8dca12da08129", + "sha256:b5f8e7d0bb2c6e6180176237f92d2e949626e04fcf701c49d73f128e1f64e1d1", + "sha256:bf922bad7df257336b594d316a1657df569860bb5389602919001fa6fb17f06e", + "sha256:c19b7d595d12923da682ed13d313c2333b9ebf214e65a47a24927a8a3a81b191", + "sha256:c1dbe65ce6d46c5f645356d64bfb2de7564e2426ca8c9b1a0a401d6f7ae5cc22", + "sha256:c3bf41b696b0fe808df1b4091c70273a52ea033b0fe97341cd67ecd76d22bb3a", + "sha256:cae08df8695f4b01fce2a8ab50e9e310971276d85dfc7103e977bed52d365094", + "sha256:cc25ad320c9b44c2d3ed33aff4f85b0b277bef4ff79b12c01ee58b52ea44be1d", + "sha256:d0ca752d51d1004caff65fccffd44b32a26cb099b546e0512cfa09facb683d6c", + "sha256:d906c00f965d5c5f4812d0086dc49bf813285ea84c97e8816405200e146f805b", + "sha256:dc4b9d96a2c689d664cf3fc7f7db46b82d2821fb2ce8a4f0798fc0a92c1569f8", + "sha256:dcdfcab60562d12dd43c1a6f495b1d089e41f0e10fac37d94db285d72b678c23", + "sha256:dcf20151d2aa451013f2b3c2cce06958f43b7573b5f616adb91786c7b777715b", + "sha256:dd7aa18db5cc826d482d876a826fee445839701f81f78567e7c74b4458d57a84", + "sha256:e04b5c10c6fcf9d9801084d1e86c9d7ada7eb48fe07ee4ae5e7fe5b1a852db8a", + "sha256:e0609e7e906c08386b7f33141254df05dcab24f1c4884150988dc7a287516aca", + "sha256:e10a02b3a31ed44c7c9a96abde335f5fa222735e73f3081d693414377eb3b016", + "sha256:e44020a4532229ccfbba353138539774686350dda71cf4368e257973dd8ba403", + "sha256:e75f9429d4533ce275c98bc68bf47fb237ae7b32c954266dabc5edab0c7d682e", + "sha256:e9064805881c30dd80a4189a0da7130e3d684de353ea36edd99c1b994bdf429e", + "sha256:e92875a315f3cc4fe7a2324c17b3c7ac5e3fd0e24b14fc4deb28370431fe6a2b", + "sha256:ea175649789f1e93edbf1a0440cab18c9838977703917221777691d8d988d7bf", + "sha256:ec5b1a338b92d1eb121e9eaf06ae3db1b9a5cd794ce318a475f6dc6f9e89c3a8", + "sha256:ecadf55ed6b8fb72e7966b52fd02919e7d7bb8e7bffeaf285803b82e774debfb", + "sha256:f4af7673e84e1ec6029f18d3a0408095c471c4e2691b6e46b4e1f0a2051734ba", + "sha256:f668035ea9faa763e8f1ea42040e8439db88cf2517056d47c348a62a257a1d02", + "sha256:f69658c42411540cf58be958a47e317fd2302cc0b613ea5cff1e60d87be2846d", + "sha256:f709169579a356132f224d525ed589f88d466bbb922b9d752d8d86b1fb57ad46", + "sha256:f7a44f8250ec178c0af703c3f1b6e81865a771272ae735ca403f27c95c62f132", + "sha256:fa637e4f314ad5b59ff6d8e809d506443d68bef30bfaecdfcfe02cce689abb2f", + "sha256:fad82e6482129c58ba9b00da6c247ab6e767645ab17981599229cce19d7b2ce9", + "sha256:fadd23eee409237fb8637a35796a6e108873c28b40f7de89a36685f18ca055ad", + "sha256:fcc212dec731aeba110953643c214982e667cab9802f7d99d066e03ba0c44c90", + "sha256:fcc329be41bb6534ecb03e50596179ab76c7643ced33d13c66967d5ae1869663", + "sha256:fd32dbca0792b9683160151dc07fad11b915020eed7c82b43faf0862c2ff06a0" ], "markers": "python_version >= '3.10'", - "version": "==0.16.0" + "version": "==0.16.2" }, "uvicorn": { "hashes": [ @@ -3610,11 +3583,11 @@ "develop": { "certifi": { "hashes": [ - "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", - "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d" + "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", + "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db" ], "markers": "python_version >= '3.7'", - "version": "==2026.5.20" + "version": "==2026.6.17" }, "cfgv": { "hashes": [ @@ -3768,11 +3741,11 @@ }, "filelock": { "hashes": [ - "sha256:7fc1b3f39cf172fd8203812043c57b8a65aef9969f38b6704f628b881f761a84", - "sha256:e58333029cc9b925f39aad59b1d8f0a1ad836af4e60d7217f4a4dba87461261d" + "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", + "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767" ], "markers": "python_version >= '3.10'", - "version": "==3.29.3" + "version": "==3.29.4" }, "identify": { "hashes": [ @@ -3851,12 +3824,12 @@ }, "pytest": { "hashes": [ - "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", - "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c" + "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", + "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==9.0.3" + "version": "==9.1.1" }, "python-discovery": { "hashes": [ @@ -3965,28 +3938,28 @@ }, "ruff": { "hashes": [ - "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", - "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", - "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", - "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", - "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", - "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", - "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", - "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", - "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", - "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", - "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", - "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", - "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", - "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", - "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", - "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", - "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", - "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d" + "sha256:01a754cd6a1b630d3f97e33eb452cf7a98040482318e870f8bc52a5a30e62657", + "sha256:02299e6e9fa5b297a3f6d5d10d7bcd655c925b028bb8b9d4588214549c6b9ec4", + "sha256:08d4c86a68f2c3ec2c9d56380a71fb4a4f65373055cbb8caabd645e9102f38d4", + "sha256:2186d9e940ae332ab293623a75b5f4fe49565f449954d50a72a046683aa6b809", + "sha256:2330215f1f393fa8733f55edce04fcf94c36a2c460fcde31f78cc84e4951e9b1", + "sha256:2698a964c70e8bf402dcb99c8810472d270d141e7aa8c4e13599fd52033a2f33", + "sha256:37e5108745c2c0705da916d7d4de533ddf547051ef45f62888c31bae73f66318", + "sha256:3fccc153a85417dcd976883160cacce486997b0a0058dd18f54b8aaaac7d1ce2", + "sha256:56949a6ce8b3abde54c0bcb22cebfe57e8771cadc84b407ae8b8eaf67ebdcd43", + "sha256:5a2c40a41a4cadbcf5897b548ab29dfe248b20c540961c0247d98a3973c70403", + "sha256:5c2abf140438032bc77b2284a6c9944ecd8a19e5f1c7b52b1b8e4a0a80d19a7a", + "sha256:5f0480ce690cbb6c4db6e5d08f19fce98e10ba131a8b60c1bcdac42771e3ae2d", + "sha256:6ba7a07e03a44dbf10bb086ee06705b173625014ec99f73a7e6836a5e5590a0c", + "sha256:8b6850172348c8381b8b3084c5915a4393c2373b9b54cd5b5e1ea15812bc10df", + "sha256:a6aa6a3d979e48ae617578183674bf264fbe7d0114a796a26bd678d67963c7ff", + "sha256:a81beadbbff2c9c245561ae3f77b16709d87f35eec650d0501679239d3449b22", + "sha256:b2c9257fcbd4a3e5b977a1904e6facca016bafe2edc17df24db67cfaee03b4e4", + "sha256:dac80dc8d26b2257dbefabed62f5d255c3937b4ccb122da1fc634794fa3578b3" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.15.17" + "version": "==0.15.18" }, "the-agent": { "editable": true, @@ -4003,11 +3976,11 @@ }, "virtualenv": { "hashes": [ - "sha256:75f4127d4067397c64f38579ce918fec6bf9ca2cd4f48685e82952cc3c035840", - "sha256:938ff0fd3f4e0f0d3a025f67a3d2f25e3c3aabbcd5857ea6170619138d72d141" + "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", + "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8" ], - "markers": "python_version >= '3.8'", - "version": "==21.4.3" + "markers": "python_version >= '3.9'", + "version": "==21.5.1" } } } diff --git a/docs/open-api-docs.yaml b/docs/open-api-docs.yaml index 1dad1463..49be53bf 100644 --- a/docs/open-api-docs.yaml +++ b/docs/open-api-docs.yaml @@ -2,7 +2,7 @@ openapi: 3.0.3 info: title: The Agent's user-facing API description: The user-facing parts of The Agent's API service (excluding system-level endpoints, chat completion, maintenance endpoints, etc.) - version: 5.19.1 + version: 5.19.2 license: name: MIT url: https://opensource.org/licenses/MIT diff --git a/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/.openspec.yaml b/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/.openspec.yaml new file mode 100644 index 00000000..95ae5a2c --- /dev/null +++ b/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-18 diff --git a/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/design.md b/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/design.md new file mode 100644 index 00000000..007c0c51 --- /dev/null +++ b/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/design.md @@ -0,0 +1,176 @@ +## Context + +`sponsorships` still uses the legacy `db/schema` + `db/crud` persistence pattern. `SponsorshipCRUD` accepts `SponsorshipSave`, returns `SponsorshipDB`, and callers convert rows with `Sponsorship.model_validate(...)`. This leaks persistence-specific Pydantic and SQLAlchemy shapes into `SponsorshipService`, `SponsorshipsController`, transfer validation, cleanup, settings checks, and tests. + +The repository pattern now exists in nearby persistence areas: + +``` +caller + │ + ▼ +repository ─────▶ SQLAlchemy DB model + │ ▲ + ▼ │ +domain dataclass ◀──── mapper +``` + +`UsageRecordRepository`, `PurchaseRecordRepository`, `ChatMembershipRepository`, and the newer chat config repository keep SQLAlchemy models at the persistence edge and return feature-level domain dataclasses to callers. + +Sponsorship differs from chat config in one important way: it does not receive partial external platform snapshots. Sponsorship rows are internal state created, accepted, and deleted by application services. `accepted_at` is the only nullable domain state: + +``` +sponsor_id / receiver_id + required composite identity, never generated + +sponsored_at + persisted timestamp, defaulted by the application domain model + existing DB default remains only as a database fallback + +accepted_at + nullable business state + None means "pending sponsorship" + non-null means "accepted sponsorship" +``` + +## Goals / Non-Goals + +**Goals:** + +- Introduce a sponsorship domain dataclass, mapper, and repository that match the repository pattern used by newer persistence units. +- Keep `SponsorshipDB` as the only SQLAlchemy model for the existing `sponsorships` table. +- Preserve all current external API behavior, internal sponsorship business rules, and database schema behavior. +- Make `sponsored_at` defaulting and `accepted_at` null semantics explicit in mapper/repository tests. +- Add repository tests beside legacy CRUD tests, then migrate production callers through the new repository/domain boundary. +- Remove legacy `db/crud/sponsorship.py` and `db/schema/sponsorship.py` only after production and test references are gone. + +**Non-Goals:** + +- No database migration or schema change. +- No endpoint, payload, response, or OpenAPI contract change. +- No change to sponsorship eligibility rules, transitive sponsorship restrictions, waitlist behavior, transfer restrictions, or cleanup retention policy. +- No user repository migration in this change, even though sponsorship flows still depend on legacy `user_crud`. + +## Decisions + +### 1. Keep One SQLAlchemy Model + +`SponsorshipDB` remains the only DB model and table definition for `sponsorships`. + +The new model introduced by this change is a feature-level domain dataclass, not a second SQLAlchemy model or table. The repository maps between the dataclass and `SponsorshipDB`. + +**Rationale**: This matches the existing repository pattern and avoids competing table definitions. The database schema already represents the desired persistence shape. + +**Alternative considered**: Create a parallel SQLAlchemy model. Rejected because it duplicates table ownership and increases Alembic risk without improving the domain boundary. + +### 2. Use One Domain Model for New and Persisted Sponsorships + +The sponsorship domain dataclass should have required composite identity fields, a non-null sponsorship timestamp default, and nullable acceptance state: + +``` +Sponsorship( + sponsor_id: UUID, + receiver_id: UUID, + sponsored_at: datetime = field(default_factory=datetime.now), + accepted_at: datetime | None = None, +) +``` + +`sponsor_id` and `receiver_id` are always required before saving. `sponsored_at` is created by the domain model and is written to the database explicitly. `accepted_at` is intentionally nullable business state. + +**Rationale**: A separate `SponsorshipSave` equivalent is unnecessary. Unlike chat config, sponsorship data is internal and does not arrive as a partial remote snapshot. Letting the domain model default `sponsored_at` keeps the app and DB representations aligned without carrying a fake nullable state. + +**Alternative considered**: Keep separate create/save and persisted domain models. Rejected because it retains the old split without adding meaningful safety. + +### 3. Write Domain State Exactly + +Repository save behavior should be explicit: + +- Insert: + - require `sponsor_id` and `receiver_id`; + - write the domain `sponsored_at` value; + - include `accepted_at` exactly as provided, including `None`. +- Update: + - locate the row by `(sponsor_id, receiver_id)`; + - write the domain `sponsored_at` value; + - write `accepted_at` exactly as provided, including `None`. + +``` + ┌──────────────────────┐ + │ Sponsorship domain │ + │ sponsor_id required │ + │ receiver_id required │ + │ sponsored_at default │ + │ accepted_at optional │ + └──────────┬───────────┘ + │ + ┌──────────────┴──────────────┐ + ▼ ▼ + no existing row existing row found + │ │ + ▼ ▼ + write sponsored_at write sponsored_at + write accepted_at write accepted_at + even when None even when None +``` + +**Rationale**: `accepted_at = None` means pending and must not be treated like an absent update value. `sponsored_at` is ordinary domain state; callers that update acceptance should preserve it by copying/replacing the existing domain model. + +**Alternative considered**: Keep `sponsored_at` nullable and let the DB generate it. Rejected because this creates ambiguity in the domain model. + +### 4. Keep Query Methods Aligned with Current CRUD Names First + +The first repository should preserve the current operation surface: + +- `get(sponsor_id, receiver_id) -> Sponsorship | None` +- `get_all_by_sponsor(sponsor_id, skip, limit) -> list[Sponsorship]` +- `get_all_by_receiver(receiver_id, skip, limit) -> list[Sponsorship]` +- `get_all(skip, limit) -> list[Sponsorship]` +- `save(sponsorship) -> Sponsorship` +- `delete(sponsor_id, receiver_id) -> Sponsorship | None` +- `delete_all_by_receiver(receiver_id) -> int` +- `delete_unaccepted_older_than(cutoff) -> int` + +**Rationale**: Matching method names keeps production migration focused on type boundaries rather than business flow redesign. + +**Alternative considered**: Rename methods around domain language, such as `find_received_by_user` or `delete_pending_before`. Rejected for the first pass because it expands review scope. + +### 5. Migrate Callers Through Service Boundaries + +The new repository should be added to DI first, while legacy CRUD remains available. Then production callers can migrate in bounded steps: + +1. Repository/domain/mapper/DI and tests only. +2. `SponsorshipService`, where creation, acceptance, and deletion state transitions are centralized. +3. `SponsorshipsController`, preserving response shape and post-create lookup behavior. +4. Secondary read/delete callers: settings checks, transfer validation, cleanup service, responder/support paths. +5. Remove legacy DI access after production callers are gone. +6. Remove legacy CRUD/schema/tests after remaining test fixtures and mocks are migrated. + +**Rationale**: Sponsorship service is the semantic owner of sponsorship state. Migrating it early lets the rest of the application consume the new domain shape without changing business rules. + +**Alternative considered**: Big-bang replace every `sponsorship_crud` and `db.schema.sponsorship` import. Rejected because controller/service tests are numerous and the boundary is easier to review in milestones. + +## Risks / Trade-offs + +- **`accepted_at=None` is mistaken for "do not update"** -> Mitigation: mapper and repository tests must explicitly cover clearing/pending behavior and accepting behavior. +- **Fresh update objects can accidentally change `sponsored_at`** -> Mitigation: service migration should fetch the existing sponsorship and use dataclass replacement for acceptance changes. +- **Mixed DB/domain sponsorship types during migration** -> Mitigation: migrate one owner boundary at a time and keep focused tests green after each milestone. +- **Controller response shape changes accidentally** -> Mitigation: use sponsorship controller tests as API behavior canaries. +- **Cleanup or transfer restrictions regress** -> Mitigation: run focused cleanup and credit transfer tests after those callers migrate. + +## Migration Plan + +1. Add the sponsorship domain model, mapper, repository, DI property, and SQL test helper. +2. Add mapper and repository tests that mirror the existing CRUD behavior and specifically cover timestamp null semantics. +3. Migrate `SponsorshipService` to the repository and domain model. +4. Migrate `SponsorshipsController` and preserve exact response behavior. +5. Migrate secondary callers in settings, transfers, cleanup, and chat responder/support paths. +6. Remove legacy DI access to `sponsorship_crud` once production callers use the repository. +7. Replace remaining test fixtures/mocks that import `SponsorshipCRUD` or `db.schema.sponsorship`. +8. Remove legacy sponsorship CRUD/schema files and obsolete CRUD tests. +9. Run the full offline test suite and pre-commit before closing implementation. + +Rollback during the transition is straightforward because the database schema does not change and legacy CRUD remains available until the final removal milestone. + +## Open Questions + +None. Sponsorship state is internal-only, and `accepted_at` remains an intentional nullable domain field. diff --git a/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/proposal.md b/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/proposal.md new file mode 100644 index 00000000..db6bd614 --- /dev/null +++ b/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/proposal.md @@ -0,0 +1,46 @@ +## Why + +Sponsorship persistence still uses the legacy `db/schema` + `db/crud` pattern, which leaks SQLAlchemy/Pydantic persistence types into controllers, services, cleanup, transfer validation, and tests. After the chat config repository migration, sponsorships are a good next candidate because the domain is small, internal-only, and has clear state transitions. + +## What Changes + +- Add a feature-level sponsorship domain model, mapper, and repository beside the existing `SponsorshipDB` SQLAlchemy model. +- Keep `SponsorshipDB` as the single database model and preserve the existing `sponsorships` table, composite primary key, foreign keys, nullable `accepted_at`, and `sponsored_at` database default as a fallback. +- Model sponsorship state explicitly as a domain dataclass: + - `sponsor_id` and `receiver_id` are always required; + - `sponsored_at` defaults to the current application time and is non-null in domain code; + - `accepted_at` remains intentionally nullable and is written exactly as domain state. +- Add the new repository to DI, migrate production callers from `sponsorship_crud` / `db.schema.sponsorship`, then remove legacy CRUD/schema access after callers and tests are migrated. +- Preserve current sponsorship behavior for sponsoring, accepting, unsponsoring, fetch responses, sponsored-user transfer restrictions, stale cleanup, and settings/sponsorship checks. +- Keep legacy CRUD tests until repository behavior is covered and production callers are migrated. + +## Capabilities + +### New Capabilities + +- `sponsorship-persistence`: Domain-model and repository behavior for creating, reading, updating, deleting, accepting, and cleaning up sponsorships without exposing SQLAlchemy DB models or legacy Pydantic persistence schemas to callers. + +### Modified Capabilities + +_(None — no existing sponsorship persistence spec.)_ + +## Impact + +**Code** +- New feature-level sponsorship files under `src/features/sponsorships/` or an equivalent feature-local package. +- `src/di/di.py` gains `sponsorship_repo`; `sponsorship_crud` is removed from DI once production callers no longer use it. +- `test/db/sql_util.py` gains a `sponsorship_repo()` helper for focused repository tests. +- Production callers are migrated from legacy CRUD/schema use to the new repository/domain model. + +**Database** +- No table, column, primary key, foreign key, default, or migration changes are intended. +- `src/db/model/sponsorship.py` remains the only SQLAlchemy representation for `sponsorships`. + +**API** +- No external API route, payload, response, or OpenAPI behavior changes are intended. +- Sponsorship endpoints should continue returning the same externally visible data while their internal persistence dependency changes. + +**Tests** +- New mapper tests verify DB/domain round trips, `sponsored_at` domain default behavior, nullable `accepted_at` handling, and composite-key identity. +- New repository tests mirror existing `test/db/crud/test_sponsorship.py` behavior and cover create/update/save/delete/bulk cleanup semantics. +- Existing sponsorship controller/service, settings, transfer, cleanup, and responder tests remain behavior canaries through migration. diff --git a/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/specs/sponsorship-persistence/spec.md b/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/specs/sponsorship-persistence/spec.md new file mode 100644 index 00000000..6695ff1f --- /dev/null +++ b/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/specs/sponsorship-persistence/spec.md @@ -0,0 +1,93 @@ +## ADDED Requirements + +### Requirement: Sponsorship repository returns domain models +The system SHALL provide a sponsorship repository that persists through the existing `SponsorshipDB` table model while accepting and returning feature-level sponsorship domain dataclasses. + +#### Scenario: Fetch by composite key returns domain model +- **WHEN** a sponsorship exists for a sponsor ID and receiver ID +- **THEN** the sponsorship repository returns a sponsorship domain dataclass with the persisted field values + +#### Scenario: Missing composite key returns none +- **WHEN** no sponsorship exists for a sponsor ID and receiver ID +- **THEN** the sponsorship repository returns `None` + +#### Scenario: Fetch by sponsor returns domain models +- **WHEN** one or more sponsorships exist for a sponsor ID +- **THEN** the sponsorship repository returns sponsorship domain dataclasses for that sponsor + +#### Scenario: Fetch by receiver returns domain models +- **WHEN** one or more sponsorships exist for a receiver ID +- **THEN** the sponsorship repository returns sponsorship domain dataclasses for that receiver + +### Requirement: Sponsorship save preserves timestamp semantics +The system SHALL preserve sponsorship timestamp behavior while replacing legacy `SponsorshipSave` persistence with a domain dataclass. + +#### Scenario: Save inserts pending sponsorship +- **WHEN** the sponsorship repository saves a domain dataclass with sponsor ID, receiver ID, app-defaulted `sponsored_at`, and `accepted_at = None` +- **THEN** the repository inserts a `sponsorships` row with that `sponsored_at` +- **THEN** the repository returns a domain dataclass with the persisted `sponsored_at` and null `accepted_at` + +#### Scenario: Save inserts accepted sponsorship +- **WHEN** the sponsorship repository saves a domain dataclass with a non-null `accepted_at` +- **THEN** the repository inserts or updates the row with that `accepted_at` +- **THEN** the repository returns a domain dataclass with the persisted `accepted_at` + +#### Scenario: Save updates acceptance without changing sponsorship time +- **WHEN** the sponsorship repository saves a domain dataclass copied from an existing sponsorship with a changed `accepted_at` +- **THEN** the repository updates `accepted_at` +- **THEN** the repository preserves the copied `sponsored_at` + +#### Scenario: Save can clear acceptance +- **WHEN** the sponsorship repository saves a domain dataclass for an existing sponsorship with `accepted_at = None` +- **THEN** the repository persists null `accepted_at` +- **THEN** the sponsorship is represented as pending + +### Requirement: Sponsorship deletion behavior is preserved +The system SHALL preserve existing sponsorship deletion behavior while moving persistence behind the repository. + +#### Scenario: Delete existing sponsorship returns deleted snapshot +- **WHEN** the sponsorship repository deletes an existing sponsorship by sponsor ID and receiver ID +- **THEN** it removes the row +- **THEN** it returns the deleted sponsorship as a domain dataclass + +#### Scenario: Delete missing sponsorship returns none +- **WHEN** the sponsorship repository deletes a missing sponsorship by sponsor ID and receiver ID +- **THEN** it returns `None` + +#### Scenario: Delete all by receiver returns deleted count +- **WHEN** the sponsorship repository deletes all sponsorships for a receiver ID +- **THEN** it removes those rows +- **THEN** it returns the number of rows deleted + +#### Scenario: Delete stale pending sponsorships returns deleted count +- **WHEN** the sponsorship repository deletes unaccepted sponsorships older than a cutoff +- **THEN** it removes only pending sponsorships older than the cutoff +- **THEN** it keeps accepted sponsorships and fresh pending sponsorships +- **THEN** it returns the number of rows deleted + +### Requirement: Production sponsorship callers use repository domain models +The system SHALL migrate production sponsorship callers from legacy sponsorship CRUD/schema usage to the repository without changing external behavior. + +#### Scenario: Sponsorship service preserves business rules +- **WHEN** sponsorship creation, acceptance, unsponsoring, self-unsponsoring, or eligibility checks run through `SponsorshipService` +- **THEN** current sponsorship business rules and result messages remain unchanged +- **THEN** sponsorship persistence uses the repository and sponsorship domain dataclass + +#### Scenario: Sponsorship controller preserves API behavior +- **WHEN** sponsorship API routes fetch, create, or delete sponsorships +- **THEN** route behavior, response shape, error behavior, and authorization behavior remain externally unchanged +- **THEN** sponsorship persistence uses repository domain models internally + +#### Scenario: Secondary callers preserve behavior +- **WHEN** settings, transfer validation, cleanup, chat responder, or support paths check or delete sponsorships +- **THEN** their externally visible behavior remains unchanged +- **THEN** they use repository domain models instead of legacy sponsorship CRUD/schema objects + +#### Scenario: DI exposes only repository access for production sponsorship persistence +- **WHEN** production callers no longer use legacy sponsorship CRUD +- **THEN** DI exposes `sponsorship_repo` for sponsorship persistence +- **THEN** DI no longer exposes `sponsorship_crud` + +#### Scenario: Legacy CRUD removed only after references are gone +- **WHEN** no production or test code imports `sponsorship_crud`, `SponsorshipCRUD`, `db.schema.sponsorship`, or `SponsorshipSave` +- **THEN** the legacy sponsorship CRUD/schema files and obsolete CRUD tests may be removed diff --git a/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/tasks.md b/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/tasks.md new file mode 100644 index 00000000..fb6c823b --- /dev/null +++ b/openspec/changes/archive/2026-06-20-migrate-sponsorship-to-repository/tasks.md @@ -0,0 +1,61 @@ +## 1. Repository Foundation + +- [x] 1.1 Create the feature-level sponsorship package/domain dataclass while leaving `SponsorshipDB` as the only SQLAlchemy model. +- [x] 1.2 Create `sponsorship_mapper.py` with DB-to-domain and domain-to-DB conversion, including composite identity, `sponsored_at`, and `accepted_at` field coverage. +- [x] 1.3 Ensure mapper insert conversion writes the domain `sponsored_at` value while the DB default remains only a fallback. +- [x] 1.4 Ensure mapper/repository update conversion writes domain state exactly and acceptance updates preserve `sponsored_at` by replacing an existing domain model. +- [x] 1.5 Create `SponsorshipRepository` with `get`, `get_all_by_sponsor`, `get_all_by_receiver`, `get_all`, `save`, `delete`, `delete_all_by_receiver`, and `delete_unaccepted_older_than`. +- [x] 1.6 Wire `sponsorship_repo` into `src/di/di.py` without removing `sponsorship_crud`. +- [x] 1.7 Add `sponsorship_repo()` to `test/db/sql_util.py`. +- [x] 1.8 Add mapper tests covering DB/domain round trips, composite key identity, pending/accepted states, `sponsored_at` domain-default behavior, and `accepted_at = None` business state. +- [x] 1.9 Add repository tests mirroring existing `test/db/crud/test_sponsorship.py` behavior plus timestamp null-semantics coverage. +- [x] 1.10 Run focused sponsorship repository and legacy CRUD tests. +- [x] 1.11 Stop for manual review of the new repository shape before migrating production callers. + +## 2. Sponsorship Service Migration + +- [x] 2.1 Migrate `SponsorshipService.sponsor_user` to create sponsorships through `sponsorship_repo.save`. +- [x] 2.2 Migrate `SponsorshipService.accept_sponsorship` to update `accepted_at` through `sponsorship_repo.save` while preserving `sponsored_at`. +- [x] 2.3 Migrate `SponsorshipService.unsponsor_by_user_id`, `unsponsor_user`, and `unsponsor_self` to repository get/delete methods. +- [x] 2.4 Replace service-level `Sponsorship.model_validate(...)` conversions with repository domain models. +- [x] 2.5 Update sponsorship service tests and mocks for repository usage and domain model return values. +- [x] 2.6 Run focused sponsorship service tests. +- [x] 2.7 Stop for manual review of sponsorship state-transition behavior. + +## 3. Sponsorship Controller Migration + +- [x] 3.1 Migrate `SponsorshipsController.fetch_sponsorships` to consume repository domain models while preserving response shape and sorting/skip behavior. +- [x] 3.2 Migrate post-create sponsorship lookup in `SponsorshipsController.sponsor_user` to `sponsorship_repo.get`. +- [x] 3.3 Update sponsorship controller tests and mocks for repository usage. +- [x] 3.4 Verify sponsorship API responses, validation errors, authorization checks, and missing-receiver behavior remain unchanged. +- [x] 3.5 Run focused sponsorship controller tests. +- [x] 3.6 Stop for manual review of sponsorship API behavior. + +## 4. Secondary Caller Migration + +- [x] 4.1 Migrate settings-controller sponsorship checks to `sponsorship_repo`. +- [x] 4.2 Migrate credit transfer sponsored-user restrictions to `sponsorship_repo`. +- [x] 4.3 Migrate cleanup stale sponsorship deletion to `sponsorship_repo`. +- [x] 4.4 Migrate chat responder, currency alert, release summary, and other support test mocks/callers that reference `sponsorship_crud`. +- [x] 4.5 Update affected unit tests and mocks for migrated secondary callers. +- [x] 4.6 Run focused settings, transfer, cleanup, responder, and support tests. +- [x] 4.7 Stop for manual review before legacy cleanup. + +## 5. Legacy Cleanup + +- [x] 5.1 Search production and test code for `sponsorship_crud`, `SponsorshipCRUD`, `db.schema.sponsorship`, `SponsorshipSave`, and `Sponsorship.model_validate`. +- [x] 5.2 Remove legacy `sponsorship_crud` access from DI after production callers are migrated. +- [x] 5.3 Replace remaining test-only `sponsorship_crud` / `SponsorshipSave` fixture setup in DB CRUD tests. +- [x] 5.4 Remove obsolete legacy sponsorship CRUD tests after repository coverage is accepted as the replacement. +- [x] 5.5 Remove legacy `db/crud/sponsorship.py`, `db/schema/sponsorship.py`, and `SQLUtil.sponsorship_crud()` after no production or test references remain. +- [x] 5.6 Confirm no production or test references to legacy sponsorship CRUD/schema remain. +- [x] 5.7 Stop for manual review before final verification. + +## 6. Final Verification + +- [x] 6.1 Run `pipenv run pytest`. +- [x] 6.2 Run `pipenv run pre-commit run --all-files --show-diff-on-failure`. +- [x] 6.3 Confirm no database migration was generated or required for this change. +- [x] 6.4 Confirm no external API/OpenAPI behavior changed. +- [x] 6.5 Validate the OpenSpec change with `openspec validate migrate-sponsorship-to-repository --strict`. +- [x] 6.6 Summarize remaining risks and completion status for final review. diff --git a/pyproject.toml b/pyproject.toml index a206b2b7..561e44d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "the-agent" -version = "5.19.1" +version = "5.19.2" [tool.setuptools] package-dir = {"" = "src"} diff --git a/src/api/settings_controller.py b/src/api/settings_controller.py index 8cf72e40..6cc7ce08 100644 --- a/src/api/settings_controller.py +++ b/src/api/settings_controller.py @@ -316,7 +316,7 @@ def save_user_settings(self, user_id_hex: str, payload: UserSettingsPayload): log.i("User settings saved") def __is_sponsored(self, user_id: UUID) -> bool: - return bool(self.__di.sponsorship_crud.get_all_by_receiver(user_id)) + return bool(self.__di.sponsorship_repo.get_all_by_receiver(user_id)) def __create_jwt_token(self, chat_type: ChatConfigDB.ChatType) -> str: external_id = resolve_external_id(self.__di.invoker, chat_type) diff --git a/src/api/sponsorships_controller.py b/src/api/sponsorships_controller.py index abcff292..4ccbbd10 100644 --- a/src/api/sponsorships_controller.py +++ b/src/api/sponsorships_controller.py @@ -3,7 +3,6 @@ from api.model.sponsorship_payload import SponsorshipPayload from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.sponsorship import Sponsorship from db.schema.user import User from di.di import DI from features.integrations.integrations import lookup_user_by_handle, resolve_any_external_handle @@ -24,20 +23,19 @@ def __init__(self, di: DI): def fetch_sponsorships(self, user_id_hex: str) -> dict[str, Any]: log.d(f"Fetching sponsorships for user '{user_id_hex}'") user = self.__di.authorization_service.authorize_for_user(self.__di.invoker, user_id_hex) - sponsorships_db = self.__di.sponsorship_crud.get_all_by_sponsor(user.id) + sponsorships = self.__di.sponsorship_repo.get_all_by_sponsor(user.id) max_sponsorships = ( config.max_sponsorships_per_user if self.__di.invoker.group != UserDB.Group.developer else config.max_users ) - if not sponsorships_db: + if not sponsorships: log.d(" No sponsorships found") return { "sponsorships": [], "max_sponsorships": max_sponsorships, } - sponsorships = [Sponsorship.model_validate(sponsorship_db) for sponsorship_db in sponsorships_db] output_sponsorships: list[dict[str, Any]] = [] for sponsorship in sponsorships: receiver_user_db = self.__di.user_crud.get(sponsorship.receiver_id) @@ -83,10 +81,9 @@ def sponsor_user(self, sponsor_user_id_hex: str, payload: SponsorshipPayload) -> if not receiver_user_db: raise InternalError("Sponsored receiver user not found after sponsorship creation", SPONSORSHIP_OPERATION_FAILED) receiver_user = User.model_validate(receiver_user_db) - sponsorship_db = self.__di.sponsorship_crud.get(user.id, receiver_user.id) - if not sponsorship_db: + sponsorship = self.__di.sponsorship_repo.get(user.id, receiver_user.id) + if not sponsorship: raise InternalError("Sponsorship row not found after sponsorship creation", SPONSORSHIP_OPERATION_FAILED) - sponsorship = Sponsorship.model_validate(sponsorship_db) platform_handle, platform_type = resolve_any_external_handle(receiver_user) log.i(f" Successfully sponsored '@{payload.platform_handle}'") return { diff --git a/src/db/crud/sponsorship.py b/src/db/crud/sponsorship.py deleted file mode 100644 index 4e2b20b4..00000000 --- a/src/db/crud/sponsorship.py +++ /dev/null @@ -1,81 +0,0 @@ -from datetime import datetime -from uuid import UUID - -from sqlalchemy.orm import Session - -from db.model.sponsorship import SponsorshipDB -from db.schema.sponsorship import SponsorshipSave - - -class SponsorshipCRUD: - - _db: Session - - def __init__(self, db: Session): - self._db = db - - def get(self, sponsor_id: UUID, receiver_id: UUID) -> SponsorshipDB | None: - return self._db.query(SponsorshipDB).filter( - SponsorshipDB.sponsor_id == sponsor_id, - SponsorshipDB.receiver_id == receiver_id, - ).first() - - def get_all_by_sponsor(self, sponsor_id: UUID, skip: int = 0, limit: int = 100) -> list[SponsorshipDB]: - # noinspection PyTypeChecker - return self._db.query(SponsorshipDB).filter( - SponsorshipDB.sponsor_id == sponsor_id, - ).offset(skip).limit(limit).all() - - def get_all_by_receiver(self, receiver_id: UUID, skip: int = 0, limit: int = 100) -> list[SponsorshipDB]: - # noinspection PyTypeChecker - return self._db.query(SponsorshipDB).filter( - SponsorshipDB.receiver_id == receiver_id, - ).offset(skip).limit(limit).all() - - def get_all(self, skip: int = 0, limit: int = 100) -> list[SponsorshipDB]: - # noinspection PyTypeChecker - return self._db.query(SponsorshipDB).offset(skip).limit(limit).all() - - def create(self, create_data: SponsorshipSave) -> SponsorshipDB: - sponsorship = SponsorshipDB(**create_data.model_dump()) - self._db.add(sponsorship) - self._db.commit() - self._db.refresh(sponsorship) - return sponsorship - - def update(self, update_data: SponsorshipSave) -> SponsorshipDB | None: - sponsorship = self.get(update_data.sponsor_id, update_data.receiver_id) - if sponsorship: - for key, value in update_data.model_dump().items(): - setattr(sponsorship, key, value) - self._db.commit() - self._db.refresh(sponsorship) - return sponsorship - - def save(self, data: SponsorshipSave) -> SponsorshipDB: - updated_sponsorship = self.update(data) - if updated_sponsorship: - return updated_sponsorship # available only if update was successful - return self.create(data) - - def delete(self, sponsor_id: UUID, receiver_id: UUID) -> SponsorshipDB | None: - sponsorship = self.get(sponsor_id, receiver_id) - if sponsorship: - self._db.delete(sponsorship) - self._db.commit() - return sponsorship - - def delete_all_by_receiver(self, receiver_id: UUID) -> int: - result = self._db.query(SponsorshipDB).filter( - SponsorshipDB.receiver_id == receiver_id, - ).delete(synchronize_session = False) # optimizes by assuming no other session will use these objects - self._db.commit() - return result - - def delete_unaccepted_older_than(self, cutoff: datetime) -> int: - deleted = self._db.query(SponsorshipDB).filter( - SponsorshipDB.accepted_at.is_(None), - SponsorshipDB.sponsored_at < cutoff, - ).delete(synchronize_session = False) - self._db.commit() - return deleted diff --git a/src/db/schema/sponsorship.py b/src/db/schema/sponsorship.py deleted file mode 100644 index 1ed232e5..00000000 --- a/src/db/schema/sponsorship.py +++ /dev/null @@ -1,19 +0,0 @@ -from datetime import datetime -from uuid import UUID - -from pydantic import BaseModel, ConfigDict - - -class SponsorshipBase(BaseModel): - sponsor_id: UUID - receiver_id: UUID - - -class SponsorshipSave(SponsorshipBase): - accepted_at: datetime | None = None - - -class Sponsorship(SponsorshipBase): - sponsored_at: datetime - accepted_at: datetime | None - model_config = ConfigDict(from_attributes = True) diff --git a/src/di/di.py b/src/di/di.py index 2db6e87d..92853d25 100644 --- a/src/di/di.py +++ b/src/di/di.py @@ -31,7 +31,6 @@ from db.crud.chat_message import ChatMessageCRUD from db.crud.chat_message_attachment import ChatMessageAttachmentCRUD from db.crud.price_alert import PriceAlertCRUD - from db.crud.sponsorship import SponsorshipCRUD from db.crud.tools_cache import ToolsCacheCRUD from db.crud.user import UserCRUD from features.accounting.purchases.purchase_record_repo import PurchaseRecordRepository @@ -87,6 +86,7 @@ from features.images.smart_image_generator import SmartImageGenerator from features.integrations.platform_bot_sdk import PlatformBotSDK from features.social_cards.social_card_orchestrator import SocialCardOrchestrator + from features.sponsorships.sponsorship_repo import SponsorshipRepository from features.sponsorships.sponsorship_service import SponsorshipService from features.support.user_support_service import UserSupportService from features.web_browsing.ai_web_search import AIWebSearch @@ -118,7 +118,7 @@ class DI: _chat_membership_service: "ChatMembershipService | None" _chat_message_crud: "ChatMessageCRUD | None" _chat_message_attachment_crud: "ChatMessageAttachmentCRUD | None" - _sponsorship_crud: "SponsorshipCRUD | None" + _sponsorship_repo: "SponsorshipRepository | None" _tools_cache_crud: "ToolsCacheCRUD | None" _price_alert_crud: "PriceAlertCRUD | None" _usage_record_repo: "UsageRecordRepository | None" @@ -177,7 +177,7 @@ def __init__( self._chat_membership_service = None self._chat_message_crud = None self._chat_message_attachment_crud = None - self._sponsorship_crud = None + self._sponsorship_repo = None self._tools_cache_crud = None self._price_alert_crud = None self._usage_record_repo = None @@ -385,11 +385,11 @@ def chat_message_attachment_crud(self) -> "ChatMessageAttachmentCRUD": return self._chat_message_attachment_crud @property - def sponsorship_crud(self) -> "SponsorshipCRUD": - if self._sponsorship_crud is None: - from db.crud.sponsorship import SponsorshipCRUD - self._sponsorship_crud = SponsorshipCRUD(self.db) - return self._sponsorship_crud + def sponsorship_repo(self) -> "SponsorshipRepository": + if self._sponsorship_repo is None: + from features.sponsorships.sponsorship_repo import SponsorshipRepository + self._sponsorship_repo = SponsorshipRepository(self.db) + return self._sponsorship_repo @property def tools_cache_crud(self) -> "ToolsCacheCRUD": diff --git a/src/features/accounting/transfers/credit_transfer_service.py b/src/features/accounting/transfers/credit_transfer_service.py index eb554df7..ab4cd207 100644 --- a/src/features/accounting/transfers/credit_transfer_service.py +++ b/src/features/accounting/transfers/credit_transfer_service.py @@ -148,10 +148,10 @@ def __validate_transfer( if sender_user.id == receiver_user.id: raise ValidationError("Cannot transfer credits to yourself", SELF_TRANSFER_NOT_ALLOWED) - if self.__di.sponsorship_crud.get_all_by_receiver(sender_user.id, limit = 1): + if self.__di.sponsorship_repo.get_all_by_receiver(sender_user.id, limit = 1): raise ValidationError("Sponsored users cannot transfer credits", SPONSORED_USER_TRANSFER_NOT_ALLOWED) - if self.__di.sponsorship_crud.get_all_by_receiver(receiver_user.id, limit = 1): + if self.__di.sponsorship_repo.get_all_by_receiver(receiver_user.id, limit = 1): raise ValidationError("Cannot transfer credits to a sponsored user", SPONSORED_USER_TRANSFER_NOT_ALLOWED) return sender_user, receiver_user diff --git a/src/features/cleanup/cleanup_service.py b/src/features/cleanup/cleanup_service.py index 94356f64..f2543f72 100644 --- a/src/features/cleanup/cleanup_service.py +++ b/src/features/cleanup/cleanup_service.py @@ -54,7 +54,7 @@ def run(self) -> CleanupResult: try: sponsorship_cutoff = datetime.now() - timedelta(days = config.cleanup_sponsorship_staleness_days) - result.sponsorships_deleted = self.__di.sponsorship_crud.delete_unaccepted_older_than(sponsorship_cutoff) + result.sponsorships_deleted = self.__di.sponsorship_repo.delete_unaccepted_older_than(sponsorship_cutoff) log.i(f" Cleanup phase 5: deleted {result.sponsorships_deleted} sponsorships") except Exception as e: log.e(f" Cleanup phase 5 (sponsorships) failed: {e}") diff --git a/src/features/external_tools/access_token_resolver.py b/src/features/external_tools/access_token_resolver.py index 20b03505..98bb6c82 100644 --- a/src/features/external_tools/access_token_resolver.py +++ b/src/features/external_tools/access_token_resolver.py @@ -3,7 +3,6 @@ from pydantic import SecretStr -from db.schema.sponsorship import Sponsorship from db.schema.user import User from di.di import DI from features.external_tools.external_tool import ExternalTool, ExternalToolProvider @@ -107,28 +106,27 @@ def __get_sponsor_user(self, user_id_hex: str) -> User | None: # cache miss - fetch from database log.t(f"Fetching sponsor info for user '{user_id_hex}' from database") user_id = UUID(hex = user_id_hex) - sponsorships_db = self.__di.sponsorship_crud.get_all_by_receiver(user_id, limit = 1) - - if not sponsorships_db: + sponsorships = self.__di.sponsorship_repo.get_all_by_receiver(user_id, limit = 1) + if not sponsorships: log.t(f"User '{user_id_hex}' has no sponsorships") self.__sponsor_cache[user_id_hex] = None return None # get sponsor user log.t("Checking sponsorships for user now") - sponsorship = Sponsorship.model_validate(sponsorships_db[0]) + sponsorship = sponsorships[0] if sponsorship.accepted_at is None: log.t(f"User '{user_id_hex}' has no accepted sponsorships") self.__sponsor_cache[user_id_hex] = None return None - sponsor_user_db = self.__di.user_crud.get(sponsorship.sponsor_id) + sponsor_user = self.__di.user_crud.get(sponsorship.sponsor_id) - if not sponsor_user_db: + if not sponsor_user: log.t(f"Sponsor '{sponsorship.sponsor_id.hex}' not found") self.__sponsor_cache[user_id_hex] = None return None - sponsor_user = User.model_validate(sponsor_user_db) + sponsor_user = User.model_validate(sponsor_user) self.__sponsor_cache[user_id_hex] = sponsor_user log.t(f"Cached sponsor '{sponsor_user.id.hex}' for user '{user_id_hex}'") return sponsor_user diff --git a/src/features/sponsorships/sponsorship.py b/src/features/sponsorships/sponsorship.py new file mode 100644 index 00000000..2ca2893d --- /dev/null +++ b/src/features/sponsorships/sponsorship.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass, field +from datetime import datetime +from uuid import UUID + + +@dataclass(kw_only = True) +class Sponsorship: + sponsor_id: UUID + receiver_id: UUID + sponsored_at: datetime = field(default_factory = datetime.now) + accepted_at: datetime | None = None diff --git a/src/features/sponsorships/sponsorship_mapper.py b/src/features/sponsorships/sponsorship_mapper.py new file mode 100644 index 00000000..1e98e4e1 --- /dev/null +++ b/src/features/sponsorships/sponsorship_mapper.py @@ -0,0 +1,28 @@ +from db.model.sponsorship import SponsorshipDB +from features.sponsorships.sponsorship import Sponsorship + + +def domain(db_model: SponsorshipDB | None) -> Sponsorship | None: + if db_model is None: + return None + + return Sponsorship( + sponsor_id = db_model.sponsor_id, + receiver_id = db_model.receiver_id, + sponsored_at = db_model.sponsored_at, + accepted_at = db_model.accepted_at, + ) + + +def db(domain_model: Sponsorship | None) -> SponsorshipDB | None: + if domain_model is None: + return None + + values = { + "sponsor_id": domain_model.sponsor_id, + "receiver_id": domain_model.receiver_id, + "sponsored_at": domain_model.sponsored_at, + "accepted_at": domain_model.accepted_at, + } + + return SponsorshipDB(**values) diff --git a/src/features/sponsorships/sponsorship_repo.py b/src/features/sponsorships/sponsorship_repo.py new file mode 100644 index 00000000..2e411dbb --- /dev/null +++ b/src/features/sponsorships/sponsorship_repo.py @@ -0,0 +1,98 @@ +from datetime import datetime +from uuid import UUID + +from sqlalchemy.orm import Session + +from db.model.sponsorship import SponsorshipDB +from features.sponsorships.sponsorship import Sponsorship +from features.sponsorships.sponsorship_mapper import db, domain + + +class SponsorshipRepository: + + _db: Session + + def __init__(self, db_session: Session): + self._db = db_session + + def get(self, sponsor_id: UUID, receiver_id: UUID) -> Sponsorship | None: + db_model = self._get_db_model(sponsor_id, receiver_id) + return domain(db_model) + + def get_all_by_sponsor( + self, + sponsor_id: UUID, + skip: int = 0, + limit: int = 100, + ) -> list[Sponsorship]: + db_models = self._db.query(SponsorshipDB).filter( + SponsorshipDB.sponsor_id == sponsor_id, + ).offset(skip).limit(limit).all() + return [domain(m) for m in db_models if m is not None] + + def get_all_by_receiver( + self, + receiver_id: UUID, + skip: int = 0, + limit: int = 100, + ) -> list[Sponsorship]: + db_models = self._db.query(SponsorshipDB).filter( + SponsorshipDB.receiver_id == receiver_id, + ).offset(skip).limit(limit).all() + return [domain(m) for m in db_models if m is not None] + + def get_all( + self, + skip: int = 0, + limit: int = 100, + ) -> list[Sponsorship]: + db_models = self._db.query(SponsorshipDB).offset(skip).limit(limit).all() + return [domain(m) for m in db_models if m is not None] + + def save(self, sponsorship: Sponsorship) -> Sponsorship: + existing = self._get_db_model(sponsorship.sponsor_id, sponsorship.receiver_id) + if existing is not None: + self.__copy_to_db_model(sponsorship, existing) + self._db.commit() + self._db.refresh(existing) + return domain(existing) + + db_model = db(sponsorship) + self._db.add(db_model) + self._db.commit() + self._db.refresh(db_model) + return domain(db_model) + + def delete(self, sponsor_id: UUID, receiver_id: UUID) -> Sponsorship | None: + db_model = self._get_db_model(sponsor_id, receiver_id) + if db_model is None: + return None + snapshot = domain(db_model) + self._db.delete(db_model) + self._db.commit() + return snapshot + + def delete_all_by_receiver(self, receiver_id: UUID) -> int: + deleted_count = self._db.query(SponsorshipDB).filter( + SponsorshipDB.receiver_id == receiver_id, + ).delete(synchronize_session = False) + self._db.commit() + return deleted_count + + def delete_unaccepted_older_than(self, cutoff: datetime) -> int: + deleted_count = self._db.query(SponsorshipDB).filter( + SponsorshipDB.accepted_at.is_(None), + SponsorshipDB.sponsored_at < cutoff, + ).delete(synchronize_session = False) + self._db.commit() + return deleted_count + + def _get_db_model(self, sponsor_id: UUID, receiver_id: UUID) -> SponsorshipDB | None: + return self._db.query(SponsorshipDB).filter( + SponsorshipDB.sponsor_id == sponsor_id, + SponsorshipDB.receiver_id == receiver_id, + ).first() + + def __copy_to_db_model(self, source: Sponsorship, target: SponsorshipDB): + target.sponsored_at = source.sponsored_at + target.accepted_at = source.accepted_at diff --git a/src/features/sponsorships/sponsorship_service.py b/src/features/sponsorships/sponsorship_service.py index a10aac41..531d21ff 100644 --- a/src/features/sponsorships/sponsorship_service.py +++ b/src/features/sponsorships/sponsorship_service.py @@ -1,10 +1,10 @@ +from dataclasses import replace from datetime import datetime from enum import Enum from uuid import UUID from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.sponsorship import Sponsorship, SponsorshipSave from db.schema.user import User from di.di import DI from features.integrations.integrations import ( @@ -13,6 +13,7 @@ resolve_external_id, resolve_user_to_save, ) +from features.sponsorships.sponsorship import Sponsorship from util import log from util.config import config @@ -49,7 +50,7 @@ def sponsor_user( return (SponsorshipService.Result.failure, message) # check if sponsor has exceeded the maximum number of sponsorships - all_sponsor_sponsorships = self.__di.sponsorship_crud.get_all_by_sponsor(sponsor_user.id) + all_sponsor_sponsorships = self.__di.sponsorship_repo.get_all_by_sponsor(sponsor_user.id) is_sponsor_developer = sponsor_user.group == UserDB.Group.developer if len(all_sponsor_sponsorships) >= config.max_sponsorships_per_user and not is_sponsor_developer: message = f"Sponsor '{sponsor_user.id}' has exceeded the maximum number of sponsorships" @@ -63,7 +64,7 @@ def sponsor_user( return (SponsorshipService.Result.failure, message) # check if sponsor is transitively sponsoring (sponsoring after being sponsored by someone else) - all_sponsorships_received_by_sponsor = self.__di.sponsorship_crud.get_all_by_receiver(sponsor_user.id) + all_sponsorships_received_by_sponsor = self.__di.sponsorship_repo.get_all_by_receiver(sponsor_user.id) if all_sponsorships_received_by_sponsor: message = f"Sponsor '{sponsor_user.id}' can't sponsor others while being sponsored themselves" log.d(message) @@ -75,7 +76,7 @@ def sponsor_user( if receiver_user_db: receiver_user = User.model_validate(receiver_user_db) # check if receiver already has a sponsorship - all_receiver_sponsorships = self.__di.sponsorship_crud.get_all_by_receiver(receiver_user.id) + all_receiver_sponsorships = self.__di.sponsorship_repo.get_all_by_receiver(receiver_user.id) if all_receiver_sponsorships: message = f"Receiver '@{receiver_handle}' already has a sponsorship" log.d(message) @@ -114,14 +115,13 @@ def sponsor_user( message = f"Sponsorship sent! Waiting for '{receiver_handle}' to send the first message" # finally, create a sponsorship to track the relationship - sponsorship_db = self.__di.sponsorship_crud.save( - SponsorshipSave( + sponsorship = self.__di.sponsorship_repo.save( + Sponsorship( sponsor_id = sponsor_user.id, receiver_id = receiver_user.id, accepted_at = accepted_at, ), ) - sponsorship = Sponsorship.model_validate(sponsorship_db) log.i(f"Sponsorship created from '{sponsorship.sponsor_id}' to '{sponsorship.receiver_id}'") return SponsorshipService.Result.success, message @@ -129,12 +129,12 @@ def unsponsor_by_user_id(self, sponsor_id_hex: str, receiver_id_hex: str) -> tup log.d(f"Unsponsoring receiver '{receiver_id_hex}' by sponsor '{sponsor_id_hex}'") sponsor_id = UUID(hex = sponsor_id_hex) receiver_id = UUID(hex = receiver_id_hex) - sponsorship_db = self.__di.sponsorship_crud.get(sponsor_id, receiver_id) - if not sponsorship_db: + sponsorship = self.__di.sponsorship_repo.get(sponsor_id, receiver_id) + if not sponsorship: message = f"No sponsorship from '{sponsor_id_hex}' to '{receiver_id_hex}'" log.d(message) return (SponsorshipService.Result.failure, message) - self.__di.sponsorship_crud.delete(sponsor_id, receiver_id) + self.__di.sponsorship_repo.delete(sponsor_id, receiver_id) log.d(f"Sponsorship from '{sponsor_id_hex}' to '{receiver_id_hex}' deleted") return (SponsorshipService.Result.success, "Sponsorship revoked!") @@ -172,12 +172,12 @@ def unsponsor_self(self, user_id_hex: str) -> tuple[Result, str]: log.d(message) return (SponsorshipService.Result.failure, message) user = User.model_validate(user_db) - sponsorships_db = self.__di.sponsorship_crud.get_all_by_receiver(user.id) - if not sponsorships_db: + sponsorships = self.__di.sponsorship_repo.get_all_by_receiver(user.id) + if not sponsorships: message = f"User '{user.id}' has no sponsorships to remove" log.d(message) return (SponsorshipService.Result.failure, message) - sponsorship = Sponsorship.model_validate(sponsorships_db[0]) + sponsorship = sponsorships[0] return self.unsponsor_by_user_id(sponsorship.sponsor_id.hex, user_id_hex) def accept_sponsorship(self, receiver: User) -> bool: @@ -189,22 +189,13 @@ def accept_sponsorship(self, receiver: User) -> bool: return False # check if user has a sponsorship - all_sponsorships = self.__di.sponsorship_crud.get_all_by_receiver(receiver.id) + all_sponsorships = self.__di.sponsorship_repo.get_all_by_receiver(receiver.id) pending_sponsorships = [sponsorship for sponsorship in all_sponsorships if sponsorship.accepted_at is None] if not pending_sponsorships: log.t(f"User '{receiver.id}' has no pending sponsorships") return False # accept the sponsorship by updating its sponsorship_at timestamp - sponsorship_db = pending_sponsorships[0] - sponsorship = Sponsorship.model_validate(sponsorship_db) - sponsorship_db = self.__di.sponsorship_crud.save( - SponsorshipSave( - sponsor_id = sponsorship.sponsor_id, - receiver_id = sponsorship.receiver_id, - accepted_at = datetime.now(), - ), - ) - sponsorship = Sponsorship.model_validate(sponsorship_db) + sponsorship = self.__di.sponsorship_repo.save(replace(pending_sponsorships[0], accepted_at = datetime.now())) log.d(f"Sponsorship from '{sponsorship.sponsor_id}' to '{sponsorship.receiver_id}' accepted") return True diff --git a/test/api/test_settings_controller.py b/test/api/test_settings_controller.py index f97b4245..0044347b 100644 --- a/test/api/test_settings_controller.py +++ b/test/api/test_settings_controller.py @@ -14,7 +14,6 @@ from api.model.user_chat_config_payload import UserChatConfigPayload from api.model.user_settings_payload import UserSettingsPayload from api.settings_controller import SettingsController -from db.crud.sponsorship import SponsorshipCRUD from db.crud.user import UserCRUD from db.model.chat_config import ChatConfigDB from db.model.user import UserDB @@ -29,6 +28,7 @@ from features.external_tools.access_token_resolver import AccessTokenResolver from features.external_tools.external_tool import CostEstimate, ExternalTool, ExternalToolProvider, ToolType from features.external_tools.external_tool_library import CLAUDE_4_6_SONNET, GPT_4O, IMAGE_GEN_FLUX_1_1, SONAR +from features.sponsorships.sponsorship_repo import SponsorshipRepository from util.config import ConfiguredProduct from util.error_codes import ( NOT_CHAT_ADMIN, @@ -48,7 +48,7 @@ class SettingsControllerTest(unittest.TestCase): mock_di: DI mock_user_dao: UserCRUD mock_chat_config_repo: ChatConfigRepository - mock_sponsorship_dao: SponsorshipCRUD + mock_sponsorship_repo: SponsorshipRepository mock_telegram_sdk: TelegramBotSDK mock_authorization_service: AuthorizationService mock_access_token_resolver: AccessTokenResolver @@ -98,14 +98,14 @@ def setUp(self): # Create mocks self.mock_user_dao = MagicMock(spec = UserCRUD) self.mock_chat_config_repo = MagicMock(spec = ChatConfigRepository) - self.mock_sponsorship_dao = MagicMock(spec = SponsorshipCRUD) + self.mock_sponsorship_repo = MagicMock(spec = SponsorshipRepository) self.mock_telegram_sdk = MagicMock(spec = TelegramBotSDK) # Configure common mock returns self.mock_user_dao.get.return_value = self.invoker_user self.mock_chat_config_repo.get.return_value = self.chat_config_domain self.mock_chat_config_repo.save.return_value = self.chat_config_domain - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] # Create mock DI container self.mock_di = MagicMock(spec = DI) @@ -120,7 +120,7 @@ def setUp(self): # noinspection PyPropertyAccess self.mock_di.chat_config_repo = self.mock_chat_config_repo # noinspection PyPropertyAccess - self.mock_di.sponsorship_crud = self.mock_sponsorship_dao + self.mock_di.sponsorship_repo = self.mock_sponsorship_repo # noinspection PyPropertyAccess self.mock_di.telegram_bot_sdk = self.mock_telegram_sdk @@ -261,7 +261,7 @@ def test_fetch_user_settings_success(self): def test_fetch_user_settings_is_sponsored_true(self): mock_sponsorship = MagicMock() mock_sponsorship.receiver_id = self.invoker_user.id - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [mock_sponsorship] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [mock_sponsorship] controller = SettingsController(self.mock_di) result = controller.fetch_user_settings(self.invoker_user.id.hex) @@ -682,7 +682,7 @@ def test_create_settings_link_with_sponsorship(self): mock_sponsorship_db = MagicMock() mock_sponsorship_db.receiver_id = self.invoker_user.id - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [mock_sponsorship_db] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [mock_sponsorship_db] controller = SettingsController(self.mock_di) link_response = controller.create_settings_link() diff --git a/test/api/test_sponsorships_controller.py b/test/api/test_sponsorships_controller.py index 34be1610..c085c99a 100644 --- a/test/api/test_sponsorships_controller.py +++ b/test/api/test_sponsorships_controller.py @@ -7,15 +7,14 @@ from api.model.sponsorship_payload import SponsorshipPayload from api.sponsorships_controller import SponsorshipsController -from db.crud.sponsorship import SponsorshipCRUD from db.crud.user import UserCRUD from db.model.chat_config import ChatConfigDB -from db.model.sponsorship import SponsorshipDB from db.model.user import UserDB -from db.schema.sponsorship import Sponsorship from db.schema.user import User from di.di import DI from features.chat.telegram.sdk.telegram_bot_sdk import TelegramBotSDK +from features.sponsorships.sponsorship import Sponsorship +from features.sponsorships.sponsorship_repo import SponsorshipRepository from features.sponsorships.sponsorship_service import SponsorshipService from util.config import config from util.errors import AuthorizationError, InternalError @@ -79,7 +78,7 @@ def setUp(self): # noinspection PyPropertyAccess self.mock_di.user_crud = Mock(spec = UserCRUD) # noinspection PyPropertyAccess - self.mock_di.sponsorship_crud = Mock(spec = SponsorshipCRUD) + self.mock_di.sponsorship_repo = Mock(spec = SponsorshipRepository) # noinspection PyPropertyAccess self.mock_di.telegram_bot_sdk = Mock(spec = TelegramBotSDK) # noinspection PyPropertyAccess @@ -105,7 +104,7 @@ def test_init_failure_invalid_user(self): self.assertIsNotNone(controller) def test_fetch_sponsorships_success_with_sponsorships(self): - sponsorship_db = SponsorshipDB( + sponsorship = Sponsorship( sponsor_id = self.sponsorship.sponsor_id, receiver_id = self.sponsorship.receiver_id, sponsored_at = self.sponsorship.sponsored_at, @@ -127,7 +126,7 @@ def test_fetch_sponsorships_success_with_sponsorships(self): are_policies_accepted = True, ) - self.mock_di.sponsorship_crud.get_all_by_sponsor.return_value = [sponsorship_db] + self.mock_di.sponsorship_repo.get_all_by_sponsor.return_value = [sponsorship] self.mock_di.user_crud.get.return_value = receiver_user_db self.mock_di.authorization_service.authorize_for_user.return_value = self.sponsor_user @@ -151,10 +150,10 @@ def test_fetch_sponsorships_success_with_sponsorships(self): # noinspection PyUnresolvedReferences self.mock_di.authorization_service.authorize_for_user.assert_called_once_with(self.invoker_user, self.sponsor_user.id.hex) # noinspection PyUnresolvedReferences - self.mock_di.sponsorship_crud.get_all_by_sponsor.assert_called_once_with(self.sponsor_user.id) + self.mock_di.sponsorship_repo.get_all_by_sponsor.assert_called_once_with(self.sponsor_user.id) def test_fetch_sponsorships_success_no_sponsorships(self): - self.mock_di.sponsorship_crud.get_all_by_sponsor.return_value = [] + self.mock_di.sponsorship_repo.get_all_by_sponsor.return_value = [] self.mock_di.authorization_service.authorize_for_user.return_value = self.sponsor_user controller = SponsorshipsController(self.mock_di) @@ -169,16 +168,16 @@ def test_fetch_sponsorships_success_no_sponsorships(self): # noinspection PyUnresolvedReferences self.mock_di.authorization_service.authorize_for_user.assert_called_once_with(self.invoker_user, self.sponsor_user.id.hex) # noinspection PyUnresolvedReferences - self.mock_di.sponsorship_crud.get_all_by_sponsor.assert_called_once_with(self.sponsor_user.id) + self.mock_di.sponsorship_repo.get_all_by_sponsor.assert_called_once_with(self.sponsor_user.id) def test_fetch_sponsorships_success_with_missing_receiver(self): - sponsorship_db = SponsorshipDB( + sponsorship = Sponsorship( sponsor_id = self.sponsorship.sponsor_id, receiver_id = self.sponsorship.receiver_id, sponsored_at = self.sponsorship.sponsored_at, accepted_at = self.sponsorship.accepted_at, ) - self.mock_di.sponsorship_crud.get_all_by_sponsor.return_value = [sponsorship_db] + self.mock_di.sponsorship_repo.get_all_by_sponsor.return_value = [sponsorship] self.mock_di.user_crud.get.return_value = None # Missing receiver self.mock_di.authorization_service.authorize_for_user.return_value = self.sponsor_user @@ -193,11 +192,11 @@ def test_fetch_sponsorships_success_with_missing_receiver(self): # noinspection PyUnresolvedReferences self.mock_di.authorization_service.authorize_for_user.assert_called_once_with(self.invoker_user, self.sponsor_user.id.hex) # noinspection PyUnresolvedReferences - self.mock_di.sponsorship_crud.get_all_by_sponsor.assert_called_once_with(self.sponsor_user.id) + self.mock_di.sponsorship_repo.get_all_by_sponsor.assert_called_once_with(self.sponsor_user.id) def test_fetch_sponsorships_success_with_null_accepted_at(self): # noinspection PyTypeChecker - sponsorship_db = SponsorshipDB( + sponsorship = Sponsorship( sponsor_id = self.sponsorship.sponsor_id, receiver_id = self.sponsorship.receiver_id, sponsored_at = self.sponsorship.sponsored_at, @@ -219,7 +218,7 @@ def test_fetch_sponsorships_success_with_null_accepted_at(self): are_policies_accepted = True, ) - self.mock_di.sponsorship_crud.get_all_by_sponsor.return_value = [sponsorship_db] + self.mock_di.sponsorship_repo.get_all_by_sponsor.return_value = [sponsorship] self.mock_di.user_crud.get.return_value = receiver_user_db self.mock_di.authorization_service.authorize_for_user.return_value = self.sponsor_user @@ -243,11 +242,11 @@ def test_fetch_sponsorships_success_with_null_accepted_at(self): # noinspection PyUnresolvedReferences self.mock_di.authorization_service.authorize_for_user.assert_called_once_with(self.invoker_user, self.sponsor_user.id.hex) # noinspection PyUnresolvedReferences - self.mock_di.sponsorship_crud.get_all_by_sponsor.assert_called_once_with(self.sponsor_user.id) + self.mock_di.sponsorship_repo.get_all_by_sponsor.assert_called_once_with(self.sponsor_user.id) def test_fetch_sponsorships_success_with_developer_user(self): developer_user = self.invoker_user.model_copy(update = {"group": UserDB.Group.developer}) - sponsorship_db = SponsorshipDB( + sponsorship = Sponsorship( sponsor_id = self.sponsorship.sponsor_id, receiver_id = self.sponsorship.receiver_id, sponsored_at = self.sponsorship.sponsored_at, @@ -271,7 +270,7 @@ def test_fetch_sponsorships_success_with_developer_user(self): # noinspection PyPropertyAccess self.mock_di.invoker = developer_user - self.mock_di.sponsorship_crud.get_all_by_sponsor.return_value = [sponsorship_db] + self.mock_di.sponsorship_repo.get_all_by_sponsor.return_value = [sponsorship] self.mock_di.user_crud.get.return_value = receiver_user_db self.mock_di.authorization_service.authorize_for_user.return_value = self.sponsor_user @@ -287,7 +286,7 @@ def test_fetch_sponsorships_success_with_developer_user(self): # noinspection PyUnresolvedReferences self.mock_di.authorization_service.authorize_for_user.assert_called_once_with(developer_user, self.sponsor_user.id.hex) # noinspection PyUnresolvedReferences - self.mock_di.sponsorship_crud.get_all_by_sponsor.assert_called_once_with(self.sponsor_user.id) + self.mock_di.sponsorship_repo.get_all_by_sponsor.assert_called_once_with(self.sponsor_user.id) def test_fetch_sponsorships_failure_unauthorized(self): self.mock_di.authorization_service.authorize_for_user.side_effect = AuthorizationError("Unauthorized", 0) @@ -320,14 +319,14 @@ def test_sponsor_user_success(self, mock_sponsor_user): is_invited_to_start = False, are_policies_accepted = True, ) - sponsorship_db = SponsorshipDB( + sponsorship = Sponsorship( sponsor_id = self.sponsor_user.id, receiver_id = self.receiver_user.id, sponsored_at = datetime.now(), accepted_at = None, ) self.mock_di.user_crud.get_by_telegram_username.return_value = receiver_user_db - self.mock_di.sponsorship_crud.get.return_value = sponsorship_db + self.mock_di.sponsorship_repo.get.return_value = sponsorship controller = SponsorshipsController(self.mock_di) payload = SponsorshipPayload(platform_handle = self.receiver_user.telegram_username, platform = "telegram") diff --git a/test/db/crud/test_sponsorship.py b/test/db/crud/test_sponsorship.py deleted file mode 100644 index 9a1ab1b3..00000000 --- a/test/db/crud/test_sponsorship.py +++ /dev/null @@ -1,214 +0,0 @@ -import unittest -from datetime import datetime, timedelta - -from db.sql_util import SQLUtil - -from db.model.sponsorship import SponsorshipDB -from db.schema.sponsorship import SponsorshipSave -from db.schema.user import UserSave - - -class SponsorshipCRUDTest(unittest.TestCase): - - sql: SQLUtil - - def setUp(self): - self.sql = SQLUtil() - - def tearDown(self): - self.sql.end_session() - - def test_create_sponsorship(self): - sponsor = self.sql.user_crud().create(UserSave()) - receiver = self.sql.user_crud().create(UserSave()) - sponsorship_data = SponsorshipSave( - sponsor_id = sponsor.id, - receiver_id = receiver.id, - ) - - sponsorship = self.sql.sponsorship_crud().create(sponsorship_data) - - self.assertEqual(sponsorship.sponsor_id, sponsorship_data.sponsor_id) - self.assertEqual(sponsorship.receiver_id, sponsorship_data.receiver_id) - self.assertIsNotNone(sponsorship.sponsored_at) - self.assertIsNone(sponsorship.accepted_at) - - def test_get_sponsorship(self): - sponsor = self.sql.user_crud().create(UserSave()) - receiver = self.sql.user_crud().create(UserSave()) - sponsorship_data = SponsorshipSave( - sponsor_id = sponsor.id, - receiver_id = receiver.id, - ) - created_sponsorship = self.sql.sponsorship_crud().create(sponsorship_data) - - fetched_sponsorship = self.sql.sponsorship_crud().get(sponsor.id, receiver.id) - - self.assertEqual(fetched_sponsorship.sponsor_id, created_sponsorship.sponsor_id) - self.assertEqual(fetched_sponsorship.receiver_id, created_sponsorship.receiver_id) - - def test_get_all_by_sponsor(self): - sponsor = self.sql.user_crud().create(UserSave()) - receiver1 = self.sql.user_crud().create(UserSave()) - receiver2 = self.sql.user_crud().create(UserSave()) - sponsorships = [ - self.sql.sponsorship_crud().create(SponsorshipSave(sponsor_id = sponsor.id, receiver_id = receiver1.id)), - self.sql.sponsorship_crud().create(SponsorshipSave(sponsor_id = sponsor.id, receiver_id = receiver2.id)), - ] - - fetched_sponsorships = self.sql.sponsorship_crud().get_all_by_sponsor(sponsor.id) - - self.assertEqual(len(fetched_sponsorships), len(sponsorships)) - for sponsorship in fetched_sponsorships: - self.assertEqual(sponsorship.sponsor_id, sponsor.id) - self.assertIn(sponsorship.receiver_id, [receiver1.id, receiver2.id]) - - def test_get_all_by_receiver(self): - receiver = self.sql.user_crud().create(UserSave()) - sponsor1 = self.sql.user_crud().create(UserSave()) - sponsor2 = self.sql.user_crud().create(UserSave()) - sponsorships = [ - self.sql.sponsorship_crud().create(SponsorshipSave(sponsor_id = sponsor1.id, receiver_id = receiver.id)), - self.sql.sponsorship_crud().create(SponsorshipSave(sponsor_id = sponsor2.id, receiver_id = receiver.id)), - ] - - fetched_sponsorships = self.sql.sponsorship_crud().get_all_by_receiver(receiver.id) - - self.assertEqual(len(fetched_sponsorships), len(sponsorships)) - for sponsorship in fetched_sponsorships: - self.assertEqual(sponsorship.receiver_id, receiver.id) - self.assertIn(sponsorship.sponsor_id, [sponsor1.id, sponsor2.id]) - - def test_get_all_sponsorships(self): - sponsor1 = self.sql.user_crud().create(UserSave()) - receiver1 = self.sql.user_crud().create(UserSave()) - sponsor2 = self.sql.user_crud().create(UserSave()) - receiver2 = self.sql.user_crud().create(UserSave()) - sponsorships = [ - self.sql.sponsorship_crud().create(SponsorshipSave(sponsor_id = sponsor1.id, receiver_id = receiver1.id)), - self.sql.sponsorship_crud().create(SponsorshipSave(sponsor_id = sponsor2.id, receiver_id = receiver2.id)), - ] - - fetched_sponsorships = self.sql.sponsorship_crud().get_all() - - self.assertEqual(len(fetched_sponsorships), len(sponsorships)) - for i in range(len(sponsorships)): - self.assertEqual(fetched_sponsorships[i].sponsor_id, sponsorships[i].sponsor_id) - self.assertEqual(fetched_sponsorships[i].receiver_id, sponsorships[i].receiver_id) - - def test_update_sponsorship(self): - sponsor = self.sql.user_crud().create(UserSave()) - receiver = self.sql.user_crud().create(UserSave()) - sponsorship_data = SponsorshipSave( - sponsor_id = sponsor.id, - receiver_id = receiver.id, - ) - created_sponsorship = self.sql.sponsorship_crud().create(sponsorship_data) - - update_data = SponsorshipSave( - sponsor_id = sponsor.id, - receiver_id = receiver.id, - accepted_at = datetime.now(), - ) - updated_sponsorship = self.sql.sponsorship_crud().update(update_data) - - self.assertEqual(updated_sponsorship.sponsor_id, created_sponsorship.sponsor_id) - self.assertEqual(updated_sponsorship.receiver_id, created_sponsorship.receiver_id) - self.assertIsNotNone(updated_sponsorship.accepted_at, created_sponsorship.sponsored_at) - self.assertEqual(updated_sponsorship.accepted_at, update_data.accepted_at) - - def test_save_sponsorship(self): - sponsor = self.sql.user_crud().create(UserSave()) - receiver = self.sql.user_crud().create(UserSave()) - sponsorship_data = SponsorshipSave( - sponsor_id = sponsor.id, - receiver_id = receiver.id, - ) - - # First, save should create the record - saved_sponsorship = self.sql.sponsorship_crud().save(sponsorship_data) - self.assertIsNotNone(saved_sponsorship) - self.assertEqual(saved_sponsorship.sponsor_id, sponsorship_data.sponsor_id) - self.assertEqual(saved_sponsorship.receiver_id, sponsorship_data.receiver_id) - self.assertIsNotNone(saved_sponsorship.sponsored_at) - self.assertIsNone(saved_sponsorship.accepted_at) - - # Now, save should update the existing record - update_data = SponsorshipSave( - sponsor_id = sponsor.id, - receiver_id = receiver.id, - accepted_at = datetime.now(), - ) - updated_sponsorship = self.sql.sponsorship_crud().save(update_data) - self.assertIsNotNone(updated_sponsorship) - self.assertEqual(updated_sponsorship.sponsor_id, sponsorship_data.sponsor_id) - self.assertEqual(updated_sponsorship.receiver_id, sponsorship_data.receiver_id) - self.assertIsNotNone(updated_sponsorship.sponsored_at) - self.assertEqual(updated_sponsorship.accepted_at, update_data.accepted_at) - - def test_delete_sponsorship(self): - sponsor = self.sql.user_crud().create(UserSave()) - receiver = self.sql.user_crud().create(UserSave()) - sponsorship_data = SponsorshipSave( - sponsor_id = sponsor.id, - receiver_id = receiver.id, - ) - created_sponsorship = self.sql.sponsorship_crud().create(sponsorship_data) - - deleted_sponsorship = self.sql.sponsorship_crud().delete(sponsor.id, receiver.id) - - self.assertEqual(deleted_sponsorship.sponsor_id, created_sponsorship.sponsor_id) - self.assertEqual(deleted_sponsorship.receiver_id, created_sponsorship.receiver_id) - self.assertIsNone(self.sql.sponsorship_crud().get(sponsor.id, receiver.id)) - - def test_delete_all_by_receiver(self): - receiver = self.sql.user_crud().create(UserSave()) - - sponsor1 = self.sql.user_crud().create(UserSave()) - sponsor2 = self.sql.user_crud().create(UserSave()) - self.sql.sponsorship_crud().create(SponsorshipSave(sponsor_id = sponsor1.id, receiver_id = receiver.id)) - self.sql.sponsorship_crud().create(SponsorshipSave(sponsor_id = sponsor2.id, receiver_id = receiver.id)) - - deleted_count = self.sql.sponsorship_crud().delete_all_by_receiver(receiver.id) - - self.assertEqual(deleted_count, 2) - remaining_sponsorships = self.sql.sponsorship_crud().get_all_by_receiver(receiver.id) - self.assertEqual(len(remaining_sponsorships), 0) - - def test_delete_unaccepted_older_than(self): - sponsor = self.sql.user_crud().create(UserSave()) - receiver1 = self.sql.user_crud().create(UserSave()) - receiver2 = self.sql.user_crud().create(UserSave()) - receiver3 = self.sql.user_crud().create(UserSave()) - - old_unaccepted = self.sql.sponsorship_crud().create( - SponsorshipSave(sponsor_id = sponsor.id, receiver_id = receiver1.id), - ) - self.sql.sponsorship_crud().create( - SponsorshipSave(sponsor_id = sponsor.id, receiver_id = receiver2.id), - ) - old_accepted = self.sql.sponsorship_crud().create( - SponsorshipSave(sponsor_id = sponsor.id, receiver_id = receiver3.id, accepted_at = datetime.now()), - ) - - session = self.sql.get_session() - old_unaccepted_db = session.query(SponsorshipDB).filter( - SponsorshipDB.sponsor_id == old_unaccepted.sponsor_id, - SponsorshipDB.receiver_id == old_unaccepted.receiver_id, - ).first() - old_unaccepted_db.sponsored_at = datetime.now() - timedelta(days = 31) - old_accepted_db = session.query(SponsorshipDB).filter( - SponsorshipDB.sponsor_id == old_accepted.sponsor_id, - SponsorshipDB.receiver_id == old_accepted.receiver_id, - ).first() - old_accepted_db.sponsored_at = datetime.now() - timedelta(days = 31) - session.commit() - - cutoff = datetime.now() - timedelta(days = 30) - - deleted_count = self.sql.sponsorship_crud().delete_unaccepted_older_than(cutoff) - - self.assertEqual(deleted_count, 1) - self.assertIsNone(self.sql.sponsorship_crud().get(sponsor.id, receiver1.id)) - self.assertIsNotNone(self.sql.sponsorship_crud().get(sponsor.id, receiver2.id)) - self.assertIsNotNone(self.sql.sponsorship_crud().get(sponsor.id, receiver3.id)) diff --git a/test/db/sql_util.py b/test/db/sql_util.py index 519c68d8..47db5204 100644 --- a/test/db/sql_util.py +++ b/test/db/sql_util.py @@ -3,7 +3,6 @@ from db.crud.chat_message import ChatMessageCRUD from db.crud.chat_message_attachment import ChatMessageAttachmentCRUD from db.crud.price_alert import PriceAlertCRUD -from db.crud.sponsorship import SponsorshipCRUD from db.crud.tools_cache import ToolsCacheCRUD from db.crud.user import UserCRUD from db.sql import initialize_db @@ -11,6 +10,7 @@ from features.accounting.usage.usage_record_repo import UsageRecordRepository from features.chat.config.chat_config_repo import ChatConfigRepository from features.chat.membership.chat_membership_repo import ChatMembershipRepository +from features.sponsorships.sponsorship_repo import SponsorshipRepository class SQLUtil: @@ -62,10 +62,10 @@ def chat_message_attachment_crud(self) -> ChatMessageAttachmentCRUD: self.start_session() return ChatMessageAttachmentCRUD(self.__session) - def sponsorship_crud(self) -> SponsorshipCRUD: + def sponsorship_repo(self) -> SponsorshipRepository: if not self.__is_session_active: self.start_session() - return SponsorshipCRUD(self.__session) + return SponsorshipRepository(self.__session) def tools_cache_crud(self) -> ToolsCacheCRUD: if not self.__is_session_active: diff --git a/test/features/accounting/transfers/test_credit_transfer_service.py b/test/features/accounting/transfers/test_credit_transfer_service.py index 952005b7..ca9c4ef1 100644 --- a/test/features/accounting/transfers/test_credit_transfer_service.py +++ b/test/features/accounting/transfers/test_credit_transfer_service.py @@ -55,7 +55,7 @@ def setUp(self): ) self.mock_di.user_crud.get_by_telegram_username.return_value = self.receiver.model_dump() self.mock_di.user_crud.update_locked_pair.return_value = None - self.mock_di.sponsorship_crud.get_all_by_receiver.return_value = [] + self.mock_di.sponsorship_repo.get_all_by_receiver.return_value = [] self.mock_di.usage_record_repo.create.return_value = None self.mock_di.clone.side_effect = Exception("notification not configured in test") @@ -167,7 +167,7 @@ def test_self_transfer_not_allowed(self): self.mock_di.user_crud.update_locked_pair.assert_not_called() def test_sponsored_sender_not_allowed(self): - self.mock_di.sponsorship_crud.get_all_by_receiver.side_effect = lambda uid, limit = 1: ( + self.mock_di.sponsorship_repo.get_all_by_receiver.side_effect = lambda uid, limit = 1: ( [Mock()] if uid == self.sender.id else [] ) @@ -183,7 +183,7 @@ def test_sponsored_sender_not_allowed(self): self.mock_di.user_crud.update_locked_pair.assert_not_called() def test_sponsored_receiver_not_allowed(self): - self.mock_di.sponsorship_crud.get_all_by_receiver.side_effect = lambda uid, limit = 1: ( + self.mock_di.sponsorship_repo.get_all_by_receiver.side_effect = lambda uid, limit = 1: ( [Mock()] if uid == self.receiver.id else [] ) diff --git a/test/features/chat/telegram/test_currency_alert_responder.py b/test/features/chat/telegram/test_currency_alert_responder.py index a4a4e0d3..cab3a0e5 100644 --- a/test/features/chat/telegram/test_currency_alert_responder.py +++ b/test/features/chat/telegram/test_currency_alert_responder.py @@ -4,7 +4,6 @@ from uuid import UUID from db.crud.price_alert import PriceAlertCRUD -from db.crud.sponsorship import SponsorshipCRUD from db.crud.tools_cache import ToolsCacheCRUD from db.crud.user import UserCRUD from db.model.chat_config import ChatConfigDB @@ -18,6 +17,7 @@ from features.chat.telegram.sdk.telegram_bot_sdk import TelegramBotSDK from features.external_tools.tool_choice_resolver import ToolChoiceResolver from features.integrations.platform_bot_sdk import PlatformBotSDK +from features.sponsorships.sponsorship_repo import SponsorshipRepository from util.translations_cache import TranslationsCache @@ -42,7 +42,7 @@ def setUp(self): # noinspection PyPropertyAccess self.mock_di.tools_cache_crud = Mock(spec = ToolsCacheCRUD) # noinspection PyPropertyAccess - self.mock_di.sponsorship_crud = Mock(spec = SponsorshipCRUD) + self.mock_di.sponsorship_repo = Mock(spec = SponsorshipRepository) # noinspection PyPropertyAccess self.mock_di.telegram_bot_sdk = Mock(spec = TelegramBotSDK) self.mock_di.telegram_bot_sdk.api = Mock(spec = TelegramBotAPI) diff --git a/test/features/chat/telegram/test_release_summary_responder.py b/test/features/chat/telegram/test_release_summary_responder.py index 93ae242f..eafe212f 100644 --- a/test/features/chat/telegram/test_release_summary_responder.py +++ b/test/features/chat/telegram/test_release_summary_responder.py @@ -8,7 +8,6 @@ from langchain_core.messages import AIMessage from api.model.release_output_payload import ReleaseOutputPayload -from db.crud.sponsorship import SponsorshipCRUD from db.crud.user import UserCRUD from db.model.chat_config import ChatConfigDB from db.model.user import UserDB @@ -31,6 +30,7 @@ from features.external_tools.tool_choice_resolver import ToolChoiceResolver from features.integrations.integrations import resolve_agent_user from features.integrations.platform_bot_sdk import PlatformBotSDK +from features.sponsorships.sponsorship_repo import SponsorshipRepository from util.translations_cache import TranslationsCache @@ -49,7 +49,7 @@ def setUp(self): # noinspection PyPropertyAccess self.mock_di.chat_config_repo = Mock(spec = ChatConfigRepository) # noinspection PyPropertyAccess - self.mock_di.sponsorship_crud = Mock(spec = SponsorshipCRUD) + self.mock_di.sponsorship_repo = Mock(spec = SponsorshipRepository) # noinspection PyPropertyAccess self.mock_di.telegram_bot_sdk = Mock(spec = TelegramBotSDK) self.mock_di.telegram_bot_sdk.api = Mock(spec = TelegramBotAPI) diff --git a/test/features/chat/telegram/test_telegram_update_responder.py b/test/features/chat/telegram/test_telegram_update_responder.py index 0c2fb4c5..5353a4de 100644 --- a/test/features/chat/telegram/test_telegram_update_responder.py +++ b/test/features/chat/telegram/test_telegram_update_responder.py @@ -99,8 +99,6 @@ def test_successful_response(self): Mock(chat_id = "123", text = "Test response"), ] - self.di.sponsorship_crud.get_by_receiver_id.return_value = [] - result = respond_to_update(self.update) self.assertTrue(result) diff --git a/test/features/chat/test_currency_alert_service.py b/test/features/chat/test_currency_alert_service.py index 27de7b29..a86f7776 100644 --- a/test/features/chat/test_currency_alert_service.py +++ b/test/features/chat/test_currency_alert_service.py @@ -6,7 +6,6 @@ from pydantic import SecretStr from db.crud.price_alert import PriceAlertCRUD -from db.crud.sponsorship import SponsorshipCRUD from db.crud.tools_cache import ToolsCacheCRUD from db.crud.user import UserCRUD from db.model.chat_config import ChatConfigDB @@ -18,6 +17,7 @@ from features.chat.currency_alert_service import CurrencyAlertService from features.chat.telegram.sdk.telegram_bot_sdk import TelegramBotSDK from features.currencies.exchange_rate_fetcher import ExchangeRateFetcher +from features.sponsorships.sponsorship_repo import SponsorshipRepository class CurrencyAlertServiceTest(unittest.TestCase): @@ -25,7 +25,7 @@ class CurrencyAlertServiceTest(unittest.TestCase): mock_user_dao: UserCRUD mock_price_alert_dao: PriceAlertCRUD mock_tools_cache_dao: ToolsCacheCRUD - mock_sponsorship_dao: SponsorshipCRUD + mock_sponsorship_repo: SponsorshipRepository mock_telegram_bot_sdk: TelegramBotSDK mock_exchange_rate_fetcher: ExchangeRateFetcher @@ -43,7 +43,7 @@ def setUp(self): self.mock_di.price_alert_crud = self.mock_price_alert_dao = MagicMock(spec = PriceAlertCRUD) self.mock_di.user_crud = self.mock_user_dao = MagicMock(spec = UserCRUD) self.mock_di.tools_cache_crud = self.mock_tools_cache_dao = MagicMock(spec = ToolsCacheCRUD) - self.mock_di.sponsorship_crud = self.mock_sponsorship_dao = MagicMock(spec = SponsorshipCRUD) + self.mock_di.sponsorship_repo = self.mock_sponsorship_repo = MagicMock(spec = SponsorshipRepository) self.mock_di.telegram_bot_sdk = self.mock_telegram_bot_sdk = MagicMock(spec = TelegramBotSDK) self.mock_di.exchange_rate_fetcher = self.mock_exchange_rate_fetcher = MagicMock(spec = ExchangeRateFetcher) self.mock_di.invoker = self.user = User( diff --git a/test/features/chat/whatsapp/test_whatsapp_update_responder.py b/test/features/chat/whatsapp/test_whatsapp_update_responder.py index 87ee9586..c4d83649 100644 --- a/test/features/chat/whatsapp/test_whatsapp_update_responder.py +++ b/test/features/chat/whatsapp/test_whatsapp_update_responder.py @@ -103,8 +103,6 @@ def test_successful_response(self): Mock(chat_id = "123", text = "Test response"), ] - self.di.sponsorship_crud.get_by_receiver_id.return_value = [] - result = respond_to_update(self.update) self.assertTrue(result) diff --git a/test/features/cleanup/test_cleanup_service.py b/test/features/cleanup/test_cleanup_service.py index 01bd02cc..7612a703 100644 --- a/test/features/cleanup/test_cleanup_service.py +++ b/test/features/cleanup/test_cleanup_service.py @@ -21,7 +21,7 @@ def _make_service( di.tools_cache_crud.delete_expired.return_value = cache_cleared di.usage_record_repo.delete_older_than.return_value = usage_deleted di.price_alert_crud.delete_stale.return_value = alerts_deleted - di.sponsorship_crud.delete_unaccepted_older_than.return_value = sponsorships_deleted + di.sponsorship_repo.delete_unaccepted_older_than.return_value = sponsorships_deleted return CleanupService(di) @patch("features.cleanup.cleanup_service.log") diff --git a/test/features/external_tools/test_access_token_resolver.py b/test/features/external_tools/test_access_token_resolver.py index c80c58d9..193443a3 100644 --- a/test/features/external_tools/test_access_token_resolver.py +++ b/test/features/external_tools/test_access_token_resolver.py @@ -1,15 +1,13 @@ import unittest +from dataclasses import replace from datetime import datetime from unittest.mock import Mock, patch from uuid import UUID from pydantic import SecretStr -from db.crud.sponsorship import SponsorshipCRUD from db.crud.user import UserCRUD -from db.model.sponsorship import SponsorshipDB from db.model.user import UserDB -from db.schema.sponsorship import Sponsorship from db.schema.user import User from di.di import DI from features.external_tools.access_token_resolver import AccessTokenResolver, ResolvedToken, TokenResolutionError @@ -25,6 +23,8 @@ XAI, X, ) +from features.sponsorships.sponsorship import Sponsorship +from features.sponsorships.sponsorship_repo import SponsorshipRepository class AccessTokenResolverTest(unittest.TestCase): @@ -33,7 +33,7 @@ class AccessTokenResolverTest(unittest.TestCase): sponsor_user: User sponsorship: Sponsorship mock_user_dao: UserCRUD - mock_sponsorship_dao: SponsorshipCRUD + mock_sponsorship_repo: SponsorshipRepository mock_di: DI openai_provider: ExternalToolProvider anthropic_provider: ExternalToolProvider @@ -84,12 +84,12 @@ def setUp(self): ) self.mock_user_dao = Mock(spec = UserCRUD) - self.mock_sponsorship_dao = Mock(spec = SponsorshipCRUD) + self.mock_sponsorship_repo = Mock(spec = SponsorshipRepository) self.mock_di = Mock(spec = DI) # noinspection PyPropertyAccess self.mock_di.user_crud = self.mock_user_dao # noinspection PyPropertyAccess - self.mock_di.sponsorship_crud = self.mock_sponsorship_dao + self.mock_di.sponsorship_repo = self.mock_sponsorship_repo # noinspection PyPropertyAccess self.mock_di.invoker = self.invoker_user @@ -113,7 +113,7 @@ def test_init_with_user_object_success(self): def test_get_access_token_success_user_has_direct_token(self): # Mock to avoid sponsorship lookup since user has direct token - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -124,16 +124,15 @@ def test_get_access_token_success_user_has_direct_token(self): self.assertEqual(token.token.get_secret_value(), self.invoker_user.open_ai_key.get_secret_value()) self.assertFalse(token.uses_credits) # noinspection PyUnresolvedReferences - self.mock_sponsorship_dao.get_all_by_receiver.assert_not_called() + self.mock_sponsorship_repo.get_all_by_receiver.assert_not_called() def test_get_access_token_success_user_no_token_has_sponsorship(self): user_without_token = self.invoker_user.model_copy(update = {"open_ai_key": None}) - sponsorship_db = SponsorshipDB(**self.sponsorship.model_dump()) sponsor_user_db = UserDB(**self.sponsor_user.model_dump()) # noinspection PyPropertyAccess self.mock_di.invoker = user_without_token - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [sponsorship_db] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [self.sponsorship] self.mock_user_dao.get.return_value = sponsor_user_db resolver = AccessTokenResolver(self.mock_di) @@ -145,28 +144,27 @@ def test_get_access_token_success_user_no_token_has_sponsorship(self): self.assertEqual(token.token.get_secret_value(), self.sponsor_user.open_ai_key.get_secret_value()) self.assertFalse(token.uses_credits) # noinspection PyUnresolvedReferences - self.mock_sponsorship_dao.get_all_by_receiver.assert_called_once_with(user_without_token.id, limit = 1) + self.mock_sponsorship_repo.get_all_by_receiver.assert_called_once_with(user_without_token.id, limit = 1) # noinspection PyUnresolvedReferences self.mock_user_dao.get.assert_called_once_with(self.sponsorship.sponsor_id) def test_get_access_token_failure_pending_sponsorship_not_accepted(self): user_without_token = self.invoker_user.model_copy(update = {"open_ai_key": None}) - pending_sponsorship = self.sponsorship.model_copy(update = {"accepted_at": None}) - sponsorship_db = SponsorshipDB(**pending_sponsorship.model_dump()) + pending_sponsorship = replace(self.sponsorship, accepted_at = None) self.mock_di.invoker = user_without_token - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [sponsorship_db] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [pending_sponsorship] resolver = AccessTokenResolver(self.mock_di) token = resolver.get_access_token(self.openai_provider) self.assertIsNone(token) - self.mock_sponsorship_dao.get_all_by_receiver.assert_called_once_with(user_without_token.id, limit = 1) + self.mock_sponsorship_repo.get_all_by_receiver.assert_called_once_with(user_without_token.id, limit = 1) self.mock_user_dao.get.assert_not_called() def test_get_access_token_failure_user_no_token_no_sponsorship(self): user_without_token = self.invoker_user.model_copy(update = {"open_ai_key": None}) - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] # noinspection PyPropertyAccess self.mock_di.invoker = user_without_token @@ -176,15 +174,14 @@ def test_get_access_token_failure_user_no_token_no_sponsorship(self): assert token is None # noinspection PyUnresolvedReferences - self.mock_sponsorship_dao.get_all_by_receiver.assert_called_once_with(user_without_token.id, limit = 1) + self.mock_sponsorship_repo.get_all_by_receiver.assert_called_once_with(user_without_token.id, limit = 1) def test_get_access_token_failure_user_no_token_sponsor_not_found(self): user_without_token = self.invoker_user.model_copy(update = {"open_ai_key": None}) - sponsorship_db = SponsorshipDB(**self.sponsorship.model_dump()) # noinspection PyPropertyAccess self.mock_di.invoker = user_without_token - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [sponsorship_db] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [self.sponsorship] self.mock_user_dao.get.return_value = None resolver = AccessTokenResolver(self.mock_di) @@ -193,19 +190,18 @@ def test_get_access_token_failure_user_no_token_sponsor_not_found(self): assert token is None # noinspection PyUnresolvedReferences - self.mock_sponsorship_dao.get_all_by_receiver.assert_called_once_with(user_without_token.id, limit = 1) + self.mock_sponsorship_repo.get_all_by_receiver.assert_called_once_with(user_without_token.id, limit = 1) # noinspection PyUnresolvedReferences self.mock_user_dao.get.assert_called_once_with(self.sponsorship.sponsor_id) def test_get_access_token_failure_user_no_token_sponsor_no_token(self): user_without_token = self.invoker_user.model_copy(update = {"open_ai_key": None}) sponsor_without_token = self.sponsor_user.model_copy(update = {"open_ai_key": None}) - sponsorship_db = SponsorshipDB(**self.sponsorship.model_dump()) sponsor_user_db = UserDB(**sponsor_without_token.model_dump()) # noinspection PyPropertyAccess self.mock_di.invoker = user_without_token - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [sponsorship_db] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [self.sponsorship] self.mock_user_dao.get.return_value = sponsor_user_db resolver = AccessTokenResolver(self.mock_di) @@ -216,7 +212,7 @@ def test_get_access_token_failure_user_no_token_sponsor_no_token(self): def test_get_access_token_failure_unsupported_provider(self): # Set up mock to return empty list to avoid sponsorship lookup since user has direct token - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] # Create a truly unsupported provider unsupported_provider = ExternalToolProvider( @@ -235,7 +231,7 @@ def test_get_access_token_failure_unsupported_provider(self): def test_get_access_token_for_tool_success(self): # Mock to avoid sponsorship lookup since user has direct token - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -247,7 +243,7 @@ def test_get_access_token_for_tool_success(self): def test_require_access_token_success(self): # Mock to avoid sponsorship lookup since user has direct token - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -259,7 +255,7 @@ def test_require_access_token_success(self): def test_require_access_token_failure_raises_exception(self): user_without_token = self.invoker_user.model_copy(update = {"open_ai_key": None}) - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] # noinspection PyPropertyAccess self.mock_di.invoker = user_without_token @@ -272,7 +268,7 @@ def test_require_access_token_failure_raises_exception(self): def test_require_access_token_for_tool_success(self): # Mock to avoid sponsorship lookup since user has direct token - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -284,7 +280,7 @@ def test_require_access_token_for_tool_success(self): def test_require_access_token_for_tool_failure_raises_exception(self): user_without_token = self.invoker_user.model_copy(update = {"open_ai_key": None}) - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] # noinspection PyPropertyAccess self.mock_di.invoker = user_without_token @@ -294,7 +290,7 @@ def test_require_access_token_for_tool_failure_raises_exception(self): resolver.require_access_token_for_tool(self.openai_tool) def test_get_access_token_anthropic_success_user_has_direct_token(self): - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -305,7 +301,7 @@ def test_get_access_token_anthropic_success_user_has_direct_token(self): self.assertEqual(token.token.get_secret_value(), self.invoker_user.anthropic_key.get_secret_value()) def test_get_access_token_perplexity_success_user_has_direct_token(self): - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -316,7 +312,7 @@ def test_get_access_token_perplexity_success_user_has_direct_token(self): self.assertEqual(token.token.get_secret_value(), self.invoker_user.perplexity_key.get_secret_value()) def test_get_access_token_replicate_success_user_has_direct_token(self): - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -327,7 +323,7 @@ def test_get_access_token_replicate_success_user_has_direct_token(self): self.assertEqual(token.token.get_secret_value(), self.invoker_user.replicate_key.get_secret_value()) def test_get_access_token_rapid_api_success_user_has_direct_token(self): - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -338,7 +334,7 @@ def test_get_access_token_rapid_api_success_user_has_direct_token(self): self.assertEqual(token.token.get_secret_value(), self.invoker_user.rapid_api_key.get_secret_value()) def test_get_access_token_coinmarketcap_success_user_has_direct_token(self): - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -349,7 +345,7 @@ def test_get_access_token_coinmarketcap_success_user_has_direct_token(self): self.assertEqual(token.token.get_secret_value(), self.invoker_user.coinmarketcap_key.get_secret_value()) def test_get_access_token_x_api_success_user_has_direct_token(self): - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -360,7 +356,7 @@ def test_get_access_token_x_api_success_user_has_direct_token(self): self.assertEqual(token.token.get_secret_value(), self.invoker_user.x_key.get_secret_value()) def test_get_access_token_x_ai_success_user_has_direct_token(self): - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -371,7 +367,7 @@ def test_get_access_token_x_ai_success_user_has_direct_token(self): self.assertEqual(token.token.get_secret_value(), self.invoker_user.x_ai_key.get_secret_value()) def test_get_access_token_google_ai_success_user_has_direct_token(self): - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -387,7 +383,7 @@ def test_get_access_token_uses_platform_key_when_user_has_credits(self): "credit_balance": 100.0, }) self.mock_di.invoker = user_with_credits - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -407,7 +403,7 @@ def test_get_access_token_returns_none_when_platform_key_is_invalid(self): "credit_balance": 100.0, }) self.mock_di.invoker = user_with_credits - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -423,10 +419,9 @@ def test_get_access_token_uses_platform_key_when_sponsored_user_has_credits(self "open_ai_key": None, "credit_balance": 50.0, }) - sponsorship_db = SponsorshipDB(**self.sponsorship.model_dump()) sponsor_user_db = UserDB(**sponsor_with_credits.model_dump()) self.mock_di.invoker = user_no_key - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [sponsorship_db] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [self.sponsorship] self.mock_user_dao.get.return_value = sponsor_user_db resolver = AccessTokenResolver(self.mock_di) @@ -446,7 +441,7 @@ def test_get_access_token_returns_none_when_credit_balance_is_zero(self): "credit_balance": 0.0, }) self.mock_di.invoker = user_zero_credits - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -462,7 +457,7 @@ def test_get_access_token_returns_none_when_credit_balance_is_negative(self): "credit_balance": -10.0, }) self.mock_di.invoker = user_negative_credits - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -478,7 +473,7 @@ def test_get_access_token_payer_id_is_invoker_when_using_platform_key(self): "credit_balance": 100.0, }) self.mock_di.invoker = user_with_credits - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -496,10 +491,9 @@ def test_get_access_token_payer_id_is_sponsor_when_sponsor_uses_platform_key(sel "open_ai_key": None, "credit_balance": 50.0, }) - sponsorship_db = SponsorshipDB(**self.sponsorship.model_dump()) sponsor_user_db = UserDB(**sponsor_with_credits.model_dump()) self.mock_di.invoker = user_no_key - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [sponsorship_db] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [self.sponsorship] self.mock_user_dao.get.return_value = sponsor_user_db resolver = AccessTokenResolver(self.mock_di) @@ -518,7 +512,7 @@ def test_platform_key_anthropic_with_credits(self): "credit_balance": 50.0, }) self.mock_di.invoker = user_with_credits - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -537,7 +531,7 @@ def test_platform_key_google_ai_with_credits(self): "credit_balance": 50.0, }) self.mock_di.invoker = user_with_credits - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -556,7 +550,7 @@ def test_platform_key_perplexity_with_credits(self): "credit_balance": 50.0, }) self.mock_di.invoker = user_with_credits - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -575,7 +569,7 @@ def test_platform_key_replicate_with_credits(self): "credit_balance": 50.0, }) self.mock_di.invoker = user_with_credits - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -594,7 +588,7 @@ def test_platform_key_rapid_api_with_credits(self): "credit_balance": 50.0, }) self.mock_di.invoker = user_with_credits - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -613,7 +607,7 @@ def test_platform_key_coinmarketcap_with_credits(self): "credit_balance": 50.0, }) self.mock_di.invoker = user_with_credits - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -632,7 +626,7 @@ def test_platform_key_x_api_with_credits(self): "credit_balance": 50.0, }) self.mock_di.invoker = user_with_credits - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) @@ -651,7 +645,7 @@ def test_platform_key_x_ai_with_credits(self): "credit_balance": 50.0, }) self.mock_di.invoker = user_with_credits - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] resolver = AccessTokenResolver(self.mock_di) diff --git a/test/features/sponsorships/test_sponsorship_mapper.py b/test/features/sponsorships/test_sponsorship_mapper.py new file mode 100644 index 00000000..8108fb35 --- /dev/null +++ b/test/features/sponsorships/test_sponsorship_mapper.py @@ -0,0 +1,109 @@ +import unittest +from datetime import datetime +from uuid import UUID + +from db.model.sponsorship import SponsorshipDB +from features.sponsorships.sponsorship import Sponsorship +from features.sponsorships.sponsorship_mapper import db, domain + + +class SponsorshipMapperTest(unittest.TestCase): + + sponsor_id: UUID + receiver_id: UUID + sponsored_at: datetime + accepted_at: datetime + + def setUp(self): + self.sponsor_id = UUID("11111111-1111-1111-1111-111111111111") + self.receiver_id = UUID("22222222-2222-2222-2222-222222222222") + self.sponsored_at = datetime(2026, 1, 1, 12, 0, 0) + self.accepted_at = datetime(2026, 1, 2, 12, 0, 0) + + def test_domain_returns_none_for_none_input(self): + self.assertIsNone(domain(None)) + + def test_db_returns_none_for_none_input(self): + self.assertIsNone(db(None)) + + def test_domain_maps_all_fields(self): + db_model = SponsorshipDB( + sponsor_id = self.sponsor_id, + receiver_id = self.receiver_id, + sponsored_at = self.sponsored_at, + accepted_at = self.accepted_at, + ) + + result = domain(db_model) + + self.assertIsNotNone(result) + self.assertEqual(result.sponsor_id, self.sponsor_id) + self.assertEqual(result.receiver_id, self.receiver_id) + self.assertEqual(result.sponsored_at, self.sponsored_at) + self.assertEqual(result.accepted_at, self.accepted_at) + + def test_domain_maps_pending_sponsorship(self): + db_model = SponsorshipDB( + sponsor_id = self.sponsor_id, + receiver_id = self.receiver_id, + sponsored_at = self.sponsored_at, + accepted_at = None, + ) + + result = domain(db_model) + + self.assertEqual(result.sponsor_id, self.sponsor_id) + self.assertEqual(result.receiver_id, self.receiver_id) + self.assertEqual(result.sponsored_at, self.sponsored_at) + self.assertIsNone(result.accepted_at) + + def test_db_maps_all_fields(self): + domain_model = Sponsorship( + sponsor_id = self.sponsor_id, + receiver_id = self.receiver_id, + sponsored_at = self.sponsored_at, + accepted_at = self.accepted_at, + ) + + result = db(domain_model) + + self.assertIsNotNone(result) + self.assertEqual(result.sponsor_id, self.sponsor_id) + self.assertEqual(result.receiver_id, self.receiver_id) + self.assertEqual(result.sponsored_at, self.sponsored_at) + self.assertEqual(result.accepted_at, self.accepted_at) + + def test_domain_defaults_sponsored_at(self): + domain_model = Sponsorship( + sponsor_id = self.sponsor_id, + receiver_id = self.receiver_id, + ) + + self.assertIsNotNone(domain_model.sponsored_at) + + def test_db_maps_default_sponsored_at(self): + domain_model = Sponsorship( + sponsor_id = self.sponsor_id, + receiver_id = self.receiver_id, + accepted_at = None, + ) + + result = db(domain_model) + + self.assertIsNotNone(result) + self.assertEqual(result.sponsor_id, self.sponsor_id) + self.assertEqual(result.receiver_id, self.receiver_id) + self.assertEqual(result.sponsored_at, domain_model.sponsored_at) + self.assertIsNone(result.accepted_at) + + def test_roundtrip_domain_to_db_to_domain(self): + original = Sponsorship( + sponsor_id = self.sponsor_id, + receiver_id = self.receiver_id, + sponsored_at = self.sponsored_at, + accepted_at = self.accepted_at, + ) + + result = domain(db(original)) + + self.assertEqual(result, original) diff --git a/test/features/sponsorships/test_sponsorship_repo.py b/test/features/sponsorships/test_sponsorship_repo.py new file mode 100644 index 00000000..7c988d42 --- /dev/null +++ b/test/features/sponsorships/test_sponsorship_repo.py @@ -0,0 +1,230 @@ +import unittest +from dataclasses import replace +from datetime import datetime, timedelta + +from db.sql_util import SQLUtil + +from db.schema.user import UserSave +from features.sponsorships.sponsorship import Sponsorship +from features.sponsorships.sponsorship_repo import SponsorshipRepository + + +class SponsorshipRepositoryTest(unittest.TestCase): + + sql: SQLUtil + repo: SponsorshipRepository + + def setUp(self): + self.sql = SQLUtil() + self.repo = self.sql.sponsorship_repo() + + def tearDown(self): + self.sql.end_session() + + def test_save_creates_pending_sponsorship(self): + sponsor = self.sql.user_crud().create(UserSave()) + receiver = self.sql.user_crud().create(UserSave()) + sponsorship = Sponsorship( + sponsor_id = sponsor.id, + receiver_id = receiver.id, + ) + + result = self.repo.save(sponsorship) + + self.assertEqual(result.sponsor_id, sponsorship.sponsor_id) + self.assertEqual(result.receiver_id, sponsorship.receiver_id) + self.assertIsNotNone(result.sponsored_at) + self.assertIsNone(result.accepted_at) + + def test_save_creates_accepted_sponsorship(self): + sponsor = self.sql.user_crud().create(UserSave()) + receiver = self.sql.user_crud().create(UserSave()) + accepted_at = datetime.now() + sponsorship = Sponsorship( + sponsor_id = sponsor.id, + receiver_id = receiver.id, + accepted_at = accepted_at, + ) + + result = self.repo.save(sponsorship) + + self.assertEqual(result.sponsor_id, sponsorship.sponsor_id) + self.assertEqual(result.receiver_id, sponsorship.receiver_id) + self.assertIsNotNone(result.sponsored_at) + self.assertEqual(result.accepted_at, accepted_at) + + def test_get_returns_saved_sponsorship(self): + sponsor = self.sql.user_crud().create(UserSave()) + receiver = self.sql.user_crud().create(UserSave()) + created = self.repo.save(Sponsorship( + sponsor_id = sponsor.id, + receiver_id = receiver.id, + )) + + result = self.repo.get(sponsor.id, receiver.id) + + self.assertIsNotNone(result) + self.assertEqual(result.sponsor_id, created.sponsor_id) + self.assertEqual(result.receiver_id, created.receiver_id) + self.assertEqual(result.sponsored_at, created.sponsored_at) + self.assertEqual(result.accepted_at, created.accepted_at) + + def test_get_returns_none_when_missing(self): + sponsor = self.sql.user_crud().create(UserSave()) + receiver = self.sql.user_crud().create(UserSave()) + + result = self.repo.get(sponsor.id, receiver.id) + + self.assertIsNone(result) + + def test_get_all_by_sponsor(self): + sponsor = self.sql.user_crud().create(UserSave()) + receiver1 = self.sql.user_crud().create(UserSave()) + receiver2 = self.sql.user_crud().create(UserSave()) + self.repo.save(Sponsorship(sponsor_id = sponsor.id, receiver_id = receiver1.id)) + self.repo.save(Sponsorship(sponsor_id = sponsor.id, receiver_id = receiver2.id)) + + results = self.repo.get_all_by_sponsor(sponsor.id) + + self.assertEqual(len(results), 2) + self.assertEqual({result.receiver_id for result in results}, {receiver1.id, receiver2.id}) + for result in results: + self.assertEqual(result.sponsor_id, sponsor.id) + + def test_get_all_by_receiver(self): + receiver = self.sql.user_crud().create(UserSave()) + sponsor1 = self.sql.user_crud().create(UserSave()) + sponsor2 = self.sql.user_crud().create(UserSave()) + self.repo.save(Sponsorship(sponsor_id = sponsor1.id, receiver_id = receiver.id)) + self.repo.save(Sponsorship(sponsor_id = sponsor2.id, receiver_id = receiver.id)) + + results = self.repo.get_all_by_receiver(receiver.id) + + self.assertEqual(len(results), 2) + self.assertEqual({result.sponsor_id for result in results}, {sponsor1.id, sponsor2.id}) + for result in results: + self.assertEqual(result.receiver_id, receiver.id) + + def test_get_all_sponsorships(self): + sponsor1 = self.sql.user_crud().create(UserSave()) + receiver1 = self.sql.user_crud().create(UserSave()) + sponsor2 = self.sql.user_crud().create(UserSave()) + receiver2 = self.sql.user_crud().create(UserSave()) + first = self.repo.save(Sponsorship(sponsor_id = sponsor1.id, receiver_id = receiver1.id)) + second = self.repo.save(Sponsorship(sponsor_id = sponsor2.id, receiver_id = receiver2.id)) + + results = self.repo.get_all() + + self.assertEqual({result.sponsor_id for result in results}, {first.sponsor_id, second.sponsor_id}) + self.assertEqual({result.receiver_id for result in results}, {first.receiver_id, second.receiver_id}) + + def test_save_updates_accepted_at_and_preserves_sponsored_at_when_replacing_existing(self): + sponsor = self.sql.user_crud().create(UserSave()) + receiver = self.sql.user_crud().create(UserSave()) + created = self.repo.save(Sponsorship( + sponsor_id = sponsor.id, + receiver_id = receiver.id, + )) + accepted_at = datetime.now() + + result = self.repo.save(replace(created, accepted_at = accepted_at)) + + self.assertEqual(result.sponsor_id, created.sponsor_id) + self.assertEqual(result.receiver_id, created.receiver_id) + self.assertEqual(result.sponsored_at, created.sponsored_at) + self.assertEqual(result.accepted_at, accepted_at) + + def test_save_can_clear_accepted_at(self): + sponsor = self.sql.user_crud().create(UserSave()) + receiver = self.sql.user_crud().create(UserSave()) + created = self.repo.save(Sponsorship( + sponsor_id = sponsor.id, + receiver_id = receiver.id, + accepted_at = datetime.now(), + )) + + result = self.repo.save(replace(created, accepted_at = None)) + + self.assertEqual(result.sponsored_at, created.sponsored_at) + self.assertIsNone(result.accepted_at) + + def test_save_can_update_explicit_sponsored_at(self): + sponsor = self.sql.user_crud().create(UserSave()) + receiver = self.sql.user_crud().create(UserSave()) + created = self.repo.save(Sponsorship( + sponsor_id = sponsor.id, + receiver_id = receiver.id, + )) + sponsored_at = datetime.now() - timedelta(days = 5) + + result = self.repo.save(replace(created, sponsored_at = sponsored_at)) + + self.assertEqual(result.sponsored_at, sponsored_at) + self.assertIsNone(result.accepted_at) + + def test_delete_sponsorship(self): + sponsor = self.sql.user_crud().create(UserSave()) + receiver = self.sql.user_crud().create(UserSave()) + created = self.repo.save(Sponsorship( + sponsor_id = sponsor.id, + receiver_id = receiver.id, + )) + + result = self.repo.delete(sponsor.id, receiver.id) + + self.assertIsNotNone(result) + self.assertEqual(result.sponsor_id, created.sponsor_id) + self.assertEqual(result.receiver_id, created.receiver_id) + self.assertIsNone(self.repo.get(sponsor.id, receiver.id)) + + def test_delete_returns_none_when_missing(self): + sponsor = self.sql.user_crud().create(UserSave()) + receiver = self.sql.user_crud().create(UserSave()) + + result = self.repo.delete(sponsor.id, receiver.id) + + self.assertIsNone(result) + + def test_delete_all_by_receiver(self): + receiver = self.sql.user_crud().create(UserSave()) + sponsor1 = self.sql.user_crud().create(UserSave()) + sponsor2 = self.sql.user_crud().create(UserSave()) + self.repo.save(Sponsorship(sponsor_id = sponsor1.id, receiver_id = receiver.id)) + self.repo.save(Sponsorship(sponsor_id = sponsor2.id, receiver_id = receiver.id)) + + deleted_count = self.repo.delete_all_by_receiver(receiver.id) + + self.assertEqual(deleted_count, 2) + self.assertEqual(len(self.repo.get_all_by_receiver(receiver.id)), 0) + + def test_delete_unaccepted_older_than(self): + sponsor = self.sql.user_crud().create(UserSave()) + receiver1 = self.sql.user_crud().create(UserSave()) + receiver2 = self.sql.user_crud().create(UserSave()) + receiver3 = self.sql.user_crud().create(UserSave()) + old_sponsored_at = datetime.now() - timedelta(days = 31) + fresh_sponsored_at = datetime.now() + + self.repo.save(Sponsorship( + sponsor_id = sponsor.id, + receiver_id = receiver1.id, + sponsored_at = old_sponsored_at, + )) + self.repo.save(Sponsorship( + sponsor_id = sponsor.id, + receiver_id = receiver2.id, + sponsored_at = fresh_sponsored_at, + )) + self.repo.save(Sponsorship( + sponsor_id = sponsor.id, + receiver_id = receiver3.id, + sponsored_at = old_sponsored_at, + accepted_at = datetime.now(), + )) + + deleted_count = self.repo.delete_unaccepted_older_than(datetime.now() - timedelta(days = 30)) + + self.assertEqual(deleted_count, 1) + self.assertIsNone(self.repo.get(sponsor.id, receiver1.id)) + self.assertIsNotNone(self.repo.get(sponsor.id, receiver2.id)) + self.assertIsNotNone(self.repo.get(sponsor.id, receiver3.id)) diff --git a/test/features/sponsorships/test_sponsorship_service.py b/test/features/sponsorships/test_sponsorship_service.py index 862067cf..a2d124b2 100644 --- a/test/features/sponsorships/test_sponsorship_service.py +++ b/test/features/sponsorships/test_sponsorship_service.py @@ -6,13 +6,13 @@ from pydantic import SecretStr -from db.crud.sponsorship import SponsorshipCRUD from db.crud.user import UserCRUD from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.sponsorship import Sponsorship from db.schema.user import User from di.di import DI +from features.sponsorships.sponsorship import Sponsorship +from features.sponsorships.sponsorship_repo import SponsorshipRepository from features.sponsorships.sponsorship_service import SponsorshipService from util.config import config @@ -21,7 +21,7 @@ class SponsorshipServiceTest(unittest.TestCase): user: User mock_user_dao: UserCRUD - mock_sponsorship_dao: SponsorshipCRUD + mock_sponsorship_repo: SponsorshipRepository mock_di: DI service: SponsorshipService @@ -42,12 +42,12 @@ def setUp(self): created_at = datetime.now().date(), ) self.mock_user_dao = Mock(spec = UserCRUD) - self.mock_sponsorship_dao = Mock(spec = SponsorshipCRUD) + self.mock_sponsorship_repo = Mock(spec = SponsorshipRepository) self.mock_di = Mock(spec = DI) # noinspection PyPropertyAccess self.mock_di.user_crud = self.mock_user_dao # noinspection PyPropertyAccess - self.mock_di.sponsorship_crud = self.mock_sponsorship_dao + self.mock_di.sponsorship_repo = self.mock_sponsorship_repo self.service = SponsorshipService(self.mock_di) def test_accept_sponsorship_success(self): @@ -63,25 +63,28 @@ def test_accept_sponsorship_success(self): }, ) - mock_sponsorship = Mock( + mock_sponsorship = Sponsorship( accepted_at = None, sponsor_id = self.user.id, receiver_id = user_without_keys.id, sponsored_at = datetime.now() - timedelta(days = 1), ) - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [mock_sponsorship] - self.mock_sponsorship_dao.save.return_value = { - "sponsor_id": mock_sponsorship.sponsor_id, - "receiver_id": mock_sponsorship.receiver_id, - "sponsored_at": mock_sponsorship.sponsored_at, - "accepted_at": datetime.now(), - } + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [mock_sponsorship] + self.mock_sponsorship_repo.save.return_value = Sponsorship( + sponsor_id = mock_sponsorship.sponsor_id, + receiver_id = mock_sponsorship.receiver_id, + sponsored_at = mock_sponsorship.sponsored_at, + accepted_at = datetime.now(), + ) result = self.service.accept_sponsorship(user_without_keys) self.assertTrue(result) # noinspection PyUnresolvedReferences - self.mock_sponsorship_dao.save.assert_called() + self.mock_sponsorship_repo.save.assert_called() + saved_sponsorship = self.mock_sponsorship_repo.save.call_args.args[0] + self.assertEqual(saved_sponsorship.sponsored_at, mock_sponsorship.sponsored_at) + self.assertIsNotNone(saved_sponsorship.accepted_at) def test_sponsor_user_success(self): sponsor_user_id_hex = self.user.id.hex @@ -105,17 +108,15 @@ def test_sponsor_user_success(self): receiver_user_db = UserDB(**receiver_user.model_dump()) self.mock_user_dao.get.return_value = self.user - self.mock_sponsorship_dao.get_all_by_sponsor.return_value = [] - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] # Ensure sponsor has no received sponsorships + self.mock_sponsorship_repo.get_all_by_sponsor.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] # Ensure sponsor has no received sponsorships self.mock_user_dao.get_by_telegram_username.return_value = None self.mock_user_dao.count.return_value = 0 self.mock_user_dao.save.return_value = receiver_user_db - self.mock_sponsorship_dao.save.return_value = { - "sponsor_id": self.user.id, - "receiver_id": receiver_user_db.id, - "sponsored_at": datetime.now(), - "accepted_at": None, - } + self.mock_sponsorship_repo.save.return_value = Sponsorship( + sponsor_id = self.user.id, + receiver_id = receiver_user_db.id, + ) result, msg = self.service.sponsor_user(sponsor_user_id_hex, receiver_telegram_username, ChatConfigDB.ChatType.telegram) @@ -153,8 +154,8 @@ def test_sponsor_user_failure_max_sponsorships_exceeded(self): receiver_telegram_username = "receiver_username" self.mock_user_dao.get.return_value = self.user - self.mock_sponsorship_dao.get_all_by_sponsor.return_value = [Mock()] * (config.max_sponsorships_per_user + 1) - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_sponsor.return_value = [Mock()] * (config.max_sponsorships_per_user + 1) + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] result, msg = self.service.sponsor_user(sponsor_user_id_hex, receiver_telegram_username, ChatConfigDB.ChatType.telegram) @@ -167,8 +168,8 @@ def test_sponsor_user_success_developer_no_limit(self): developer_user = self.user.model_copy(update = {"group": UserDB.Group.developer}) self.mock_user_dao.get.return_value = developer_user - self.mock_sponsorship_dao.get_all_by_sponsor.return_value = [Mock()] * (config.max_sponsorships_per_user + 1) - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_sponsor.return_value = [Mock()] * (config.max_sponsorships_per_user + 1) + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] self.mock_user_dao.get_by_telegram_username.return_value = None self.mock_user_dao.count.return_value = 0 @@ -191,14 +192,10 @@ def test_sponsor_user_success_developer_no_limit(self): self.mock_user_dao.save.return_value = new_user - # Create a more specific mock for the new sponsorship - mock_sponsorship: Sponsorship = Mock(spec = Sponsorship) - mock_sponsorship.sponsor_id = developer_user.id - mock_sponsorship.receiver_id = new_user.id - mock_sponsorship.sponsored_at = datetime.now() - mock_sponsorship.accepted_at = None - - self.mock_sponsorship_dao.save.return_value = mock_sponsorship + self.mock_sponsorship_repo.save.return_value = Sponsorship( + sponsor_id = developer_user.id, + receiver_id = new_user.id, + ) result, msg = self.service.sponsor_user(sponsor_user_id_hex, receiver_telegram_username, ChatConfigDB.ChatType.telegram) @@ -223,17 +220,15 @@ def test_sponsor_user_at_capacity_creates_waitlisted_user(self): are_policies_accepted = False, ) self.mock_user_dao.get.return_value = self.user - self.mock_sponsorship_dao.get_all_by_sponsor.return_value = [] - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_sponsor.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] self.mock_user_dao.get_by_telegram_username.return_value = None self.mock_user_dao.count.return_value = config.max_users self.mock_user_dao.save.return_value = receiver_user_db - self.mock_sponsorship_dao.save.return_value = { - "sponsor_id": self.user.id, - "receiver_id": receiver_user_db.id, - "sponsored_at": datetime.now(), - "accepted_at": None, - } + self.mock_sponsorship_repo.save.return_value = Sponsorship( + sponsor_id = self.user.id, + receiver_id = receiver_user_db.id, + ) result, _ = self.service.sponsor_user( sponsor_user_id_hex, @@ -265,8 +260,8 @@ def test_sponsor_user_failure_no_api_key(self): self.mock_user_dao.get.return_value = sponsor_without_keys # Mock the sponsorship checks that come before API key validation - self.mock_sponsorship_dao.get_all_by_sponsor.return_value = [] - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_sponsor.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] result, msg = self.service.sponsor_user(sponsor_user_id_hex, receiver_telegram_username, ChatConfigDB.ChatType.telegram) @@ -278,8 +273,8 @@ def test_sponsor_user_failure_transitive_sponsorship(self): receiver_telegram_username = "receiver_username" self.mock_user_dao.get.return_value = self.user - self.mock_sponsorship_dao.get_all_by_sponsor.return_value = [] - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [Mock()] + self.mock_sponsorship_repo.get_all_by_sponsor.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [Mock()] result, msg = self.service.sponsor_user(sponsor_user_id_hex, receiver_telegram_username, ChatConfigDB.ChatType.telegram) @@ -301,11 +296,8 @@ def test_sponsor_user_failure_receiver_has_sponsorship(self): ) self.mock_user_dao.get.return_value = self.user self.mock_user_dao.get_by_telegram_username.return_value = receiver_user - self.mock_sponsorship_dao.get_all_by_receiver.side_effect = [ - [], - [Mock(spec = Sponsorship)], - ] - self.mock_sponsorship_dao.get_all_by_sponsor.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.side_effect = [[], [Mock(spec = Sponsorship)]] + self.mock_sponsorship_repo.get_all_by_sponsor.return_value = [] result, msg = self.service.sponsor_user(sponsor_user_id_hex, receiver_telegram_username, ChatConfigDB.ChatType.telegram) @@ -333,8 +325,8 @@ def test_sponsor_user_failure_receiver_has_api_key(self): ) self.mock_user_dao.get.return_value = self.user - self.mock_sponsorship_dao.get_all_by_sponsor.return_value = [] - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] # No transitive sponsoring + self.mock_sponsorship_repo.get_all_by_sponsor.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] # No transitive sponsoring self.mock_user_dao.get_by_telegram_username.return_value = receiver_user result, msg = self.service.sponsor_user(sponsor_user_id_hex, receiver_telegram_username, ChatConfigDB.ChatType.telegram) @@ -365,7 +357,7 @@ def test_unsponsor_user_success(self): receiver_user_db = UserDB(**receiver_user.model_dump()) # Create a mock sponsorship - sponsorship_db = Sponsorship( + sponsorship = Sponsorship( sponsor_id = sponsor_user_db.id, receiver_id = receiver_user_db.id, sponsored_at = datetime.now(), @@ -374,14 +366,14 @@ def test_unsponsor_user_success(self): self.mock_user_dao.get.side_effect = [sponsor_user_db, receiver_user_db] self.mock_user_dao.get_by_telegram_username.return_value = receiver_user_db - self.mock_sponsorship_dao.get.return_value = Mock(**sponsorship_db.model_dump()) + self.mock_sponsorship_repo.get.return_value = sponsorship result, msg = self.service.unsponsor_user(sponsor_user_id_hex, receiver_telegram_username, ChatConfigDB.ChatType.telegram) self.assertEqual(result, SponsorshipService.Result.success) self.assertIn("Sponsorship revoked", msg) # noinspection PyUnresolvedReferences - self.mock_sponsorship_dao.delete.assert_called_once_with(sponsor_user_db.id, receiver_user_db.id) + self.mock_sponsorship_repo.delete.assert_called_once_with(sponsor_user_db.id, receiver_user_db.id) # Token removal is no longer handled by SponsorshipService # noinspection PyUnresolvedReferences self.mock_user_dao.save.assert_not_called() @@ -421,7 +413,7 @@ def test_unsponsor_user_failure_no_sponsorship(self): self.mock_user_dao.get.side_effect = [sponsor_user_db, receiver_user_db] self.mock_user_dao.get_by_telegram_username.return_value = receiver_user_db - self.mock_sponsorship_dao.get.return_value = None + self.mock_sponsorship_repo.get.return_value = None result, msg = self.service.unsponsor_user(sponsor_user_id_hex, receiver_telegram_username, ChatConfigDB.ChatType.telegram) @@ -429,7 +421,7 @@ def test_unsponsor_user_failure_no_sponsorship(self): self.assertIn("No sponsorship", msg) def test_accept_sponsorship_failure_no_sponsorship(self): - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] result = self.service.accept_sponsorship(self.user) @@ -463,12 +455,20 @@ def test_accept_sponsorship_success_no_api_key(self): sponsored_at = datetime.now(), accepted_at = None, ) - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [pending_sponsorship] - self.mock_sponsorship_dao.save.return_value = pending_sponsorship + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [pending_sponsorship] + self.mock_sponsorship_repo.save.return_value = Sponsorship( + sponsor_id = pending_sponsorship.sponsor_id, + receiver_id = pending_sponsorship.receiver_id, + sponsored_at = pending_sponsorship.sponsored_at, + accepted_at = datetime.now(), + ) result = self.service.accept_sponsorship(user_without_keys) self.assertTrue(result) + saved_sponsorship = self.mock_sponsorship_repo.save.call_args.args[0] + self.assertEqual(saved_sponsorship.sponsored_at, pending_sponsorship.sponsored_at) + self.assertIsNotNone(saved_sponsorship.accepted_at) # === unsponsor_by_user_id === @@ -480,24 +480,24 @@ def test_unsponsor_by_user_id_success(self): sponsored_at = datetime.now(), accepted_at = datetime.now(), ) - self.mock_sponsorship_dao.get.return_value = sponsorship_db + self.mock_sponsorship_repo.get.return_value = sponsorship_db result, msg = self.service.unsponsor_by_user_id(sponsor_id.hex, self.user.id.hex) self.assertEqual(result, SponsorshipService.Result.success) self.assertIn("Sponsorship revoked", msg) # noinspection PyUnresolvedReferences - self.mock_sponsorship_dao.delete.assert_called_once_with(sponsor_id, self.user.id) + self.mock_sponsorship_repo.delete.assert_called_once_with(sponsor_id, self.user.id) def test_unsponsor_by_user_id_failure_no_sponsorship(self): - self.mock_sponsorship_dao.get.return_value = None + self.mock_sponsorship_repo.get.return_value = None result, msg = self.service.unsponsor_by_user_id(UUID(int = 2).hex, self.user.id.hex) self.assertEqual(result, SponsorshipService.Result.failure) self.assertIn("No sponsorship", msg) # noinspection PyUnresolvedReferences - self.mock_sponsorship_dao.delete.assert_not_called() + self.mock_sponsorship_repo.delete.assert_not_called() # === unsponsor_self === @@ -512,15 +512,15 @@ def test_unsponsor_self_success(self): accepted_at = datetime.now(), ) self.mock_user_dao.get.return_value = user_db - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [sponsorship_db] - self.mock_sponsorship_dao.get.return_value = sponsorship_db + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [sponsorship_db] + self.mock_sponsorship_repo.get.return_value = sponsorship_db result, msg = self.service.unsponsor_self(user_id_hex) self.assertEqual(result, SponsorshipService.Result.success) self.assertIn("Sponsorship revoked", msg) # noinspection PyUnresolvedReferences - self.mock_sponsorship_dao.delete.assert_called_once_with(sponsor_id, self.user.id) + self.mock_sponsorship_repo.delete.assert_called_once_with(sponsor_id, self.user.id) def test_unsponsor_self_failure_user_not_found(self): self.mock_user_dao.get.return_value = None @@ -532,7 +532,7 @@ def test_unsponsor_self_failure_user_not_found(self): def test_unsponsor_self_failure_no_sponsorships(self): self.mock_user_dao.get.return_value = UserDB(**self.user.model_dump()) - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [] result, msg = self.service.unsponsor_self(self.user.id.hex) @@ -549,7 +549,7 @@ def test_unsponsor_self_delegates_to_unsponsor_by_user_id(self): accepted_at = datetime.now(), ) self.mock_user_dao.get.return_value = user_db - self.mock_sponsorship_dao.get_all_by_receiver.return_value = [sponsorship_db] + self.mock_sponsorship_repo.get_all_by_receiver.return_value = [sponsorship_db] with unittest.mock.patch.object(self.service, "unsponsor_by_user_id") as mock_method: mock_method.return_value = (SponsorshipService.Result.success, "Revoked")