From c9ab8ef76f6d966c7bf72a4b5439403f73e51610 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 08:14:02 +0000 Subject: [PATCH 1/7] Bump the pip-all group with 8 updates Bumps the pip-all group with 8 updates: | Package | From | To | | --- | --- | --- | | [langchain](https://github.com/langchain-ai/langchain) | `1.3.4` | `1.3.9` | | [langchain-core](https://github.com/langchain-ai/langchain) | `1.4.1` | `1.4.7` | | [langchain-openai](https://github.com/langchain-ai/langchain) | `1.2.2` | `1.3.2` | | [langchain-anthropic](https://github.com/langchain-ai/langchain) | `1.4.4` | `1.4.6` | | [langchain-google-genai](https://github.com/langchain-ai/langchain-google) | `4.2.4` | `4.2.5` | | [langchain-perplexity](https://github.com/langchain-ai/langchain) | `1.3.2` | `1.4.0` | | [xai-sdk](https://github.com/xai-org/xai-sdk-python) | `1.15.0` | `1.17.0` | | [ruff](https://github.com/astral-sh/ruff) | `0.15.16` | `0.15.17` | Updates `langchain` from 1.3.4 to 1.3.9 - [Release notes](https://github.com/langchain-ai/langchain/releases) - [Commits](https://github.com/langchain-ai/langchain/compare/langchain==1.3.4...langchain==1.3.9) Updates `langchain-core` from 1.4.1 to 1.4.7 - [Release notes](https://github.com/langchain-ai/langchain/releases) - [Commits](https://github.com/langchain-ai/langchain/compare/langchain-core==1.4.1...langchain-core==1.4.7) Updates `langchain-openai` from 1.2.2 to 1.3.2 - [Release notes](https://github.com/langchain-ai/langchain/releases) - [Commits](https://github.com/langchain-ai/langchain/compare/langchain-openai==1.2.2...langchain-openai==1.3.2) Updates `langchain-anthropic` from 1.4.4 to 1.4.6 - [Release notes](https://github.com/langchain-ai/langchain/releases) - [Commits](https://github.com/langchain-ai/langchain/compare/langchain-anthropic==1.4.4...langchain-anthropic==1.4.6) Updates `langchain-google-genai` from 4.2.4 to 4.2.5 - [Release notes](https://github.com/langchain-ai/langchain-google/releases) - [Commits](https://github.com/langchain-ai/langchain-google/compare/libs/genai/v4.2.4...libs/genai/v4.2.5) Updates `langchain-perplexity` from 1.3.2 to 1.4.0 - [Release notes](https://github.com/langchain-ai/langchain/releases) - [Commits](https://github.com/langchain-ai/langchain/compare/langchain-perplexity==1.3.2...langchain-perplexity==1.4.0) Updates `xai-sdk` from 1.15.0 to 1.17.0 - [Release notes](https://github.com/xai-org/xai-sdk-python/releases) - [Changelog](https://github.com/xai-org/xai-sdk-python/blob/main/CHANGELOG.md) - [Commits](https://github.com/xai-org/xai-sdk-python/compare/v1.15.0...v1.17.0) Updates `ruff` from 0.15.16 to 0.15.17 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.15.16...0.15.17) --- updated-dependencies: - dependency-name: langchain dependency-version: 1.3.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: pip-all - dependency-name: langchain-core dependency-version: 1.4.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: pip-all - dependency-name: langchain-openai dependency-version: 1.3.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: pip-all - dependency-name: langchain-anthropic dependency-version: 1.4.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: pip-all - dependency-name: langchain-google-genai dependency-version: 4.2.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: pip-all - dependency-name: langchain-perplexity dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: pip-all - dependency-name: xai-sdk dependency-version: 1.17.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: pip-all - dependency-name: ruff dependency-version: 0.15.17 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: pip-all ... Signed-off-by: dependabot[bot] --- Pipfile.lock | 569 +++++++++++++++++++++++++-------------------------- 1 file changed, 283 insertions(+), 286 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 4ac49d80..269d326d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -27,128 +27,128 @@ }, "aiohttp": { "hashes": [ - "sha256:02cb2ffbb7da32f82e21ad9952669c45bd88a80e0878264c2f59fe1c6fb2badd", - "sha256:0746d9fb0ac4fdef643a84494efe3f06d50335dd8c7a530228b86448aae0a803", - "sha256:076cb014191ae2e65d949e1ad01f1dcfe33e32789b5172510f3e79c79fc04d50", - "sha256:0fc2b75ae8d169d853be2862d960be8550da6c5c65711d5476407eb3fdb006bd", - "sha256:101df7779c80c0636014a6b2c6642acd3efb5b355d48347c9d7dfb720aee9430", - "sha256:106ed074a856f3e21d186b8579e2c8afb6da598e267cdaab01059e13db2fc44d", - "sha256:1210d4c87cc00128160c7384ab41877a701295b97cffa6362f908a49b6e8a7ca", - "sha256:1394dce36e0f0d260ac0b555a654de19cb989f3c1b8bdd24f505314dfea18a00", - "sha256:145262119b07d7f95abc1839add35ba2bfc84551d4b4660ca11542c0b215455b", - "sha256:16eee56bcc72d04600bc56c1759982c2385ec0b41d3fd3521f836bf64a0957ef", - "sha256:198cfe61bf253b19da1fb3e0fa122249dc4f14c12709493fed8054aa0411cc76", - "sha256:19ca5fc84130675ba11c6ca5c7da5cb65f7bf8a32cdd2b616bf49cd334688aae", - "sha256:1a4a9f17e85b80878c176695c1998c790e83731d8271881e5d356488652a1f9e", - "sha256:1a78a77366ed158a0a54b076990e575d7b7cdb728cbfd02711eadab150f2269f", - "sha256:20144819e99db593e22bbd2f3f2691a5e149f879142d6b8670254708853ff4fb", - "sha256:22a8d06f204e0518a586d770032db3c7043c9ba3693081b3e3ad425e1458d594", - "sha256:23e8314e7aed8576fbe33314d218bd81447a3adbc91dc36f1163bf583cd3084c", - "sha256:23f094a1ef64823fd35854ddf5c7a80a078162f37f9d2f7c6142b51a6affa456", - "sha256:25400d710641a8040bf022a8a99f579e581ffa1c5bd42c33255d7d6f3957c127", - "sha256:25d2326a4967bf705a9f9913a13005e93b6020ad8a9f6bd6bd78850d5171332e", - "sha256:25e9f1d2465a210d60edb64d7b204a147e85d4c194eecef3d1604fb5ace678ce", - "sha256:26b6d79aa54cb4ed50cc7d41ed14e99e0f1fc8e7c2d42f2e05b37aea897b2b52", - "sha256:26d9224c6dd7f5c749aba4f61315a894601448b28d94d12f4dea0903e26d2096", - "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", - "sha256:28eee8de1d69711c53116df8202f1c2aa0e3f80ef912a88fc18d159d53e7110b", - "sha256:2c2c7e05dd5335b298085abf45ddf98673934c3ee1c083d0b9ea13d4186ad500", - "sha256:2cc736a9c9fc2bc4dd71fd404815741b6573df27c3f985948ec4076989ac57de", - "sha256:2d2ffe9b614f50f069068b3b52e73414e4107fc10b7efc939a76acff9251fdd2", - "sha256:2e2514cb7195f6d7c219339635bea71ae47d1569b051300d32df9dcfabcdb869", - "sha256:2f3fc37054564dee64a855b5b092d87ec35dcddfaabf7dacb1c8a2b1f83dc0a9", - "sha256:30e8b7eeb42d02c120ca90d6c6e076a221a16b70a6dac9ae44c7ab5104cc7fe4", - "sha256:32e735c3182de7b64f6941a4ede48b38c7f47d9437bd615dd30b5bda8fa1bc93", - "sha256:3366751d68d237c621264233a32f3078bbc21b7904ab90a77e03d21390c742c6", - "sha256:363ef9e91014e7891679bfb2ac0a7c6ea93435dbbfd10ecf41b9f06fcf506c5f", - "sha256:3b54fbff46127aeafdd764cecd0d99fa2f24a0e37ea5c18a7c3a4ac450df1db3", - "sha256:3c7139100fbaae76515b73051d8f0aa3a3ff02e415eec8a8eee8e2223d9ba955", - "sha256:3cdf534aa455593e589302990c5097aa5c92c06c4262a20da22934f9186a5fff", - "sha256:3ea81eb518a2ecb319d8ec6d1424a37c773f6634bd87d6985eb606b2faac419f", - "sha256:40ae7b0642c25632c7eabc4a04754012691864d2a1b93becf7cddb76027b838a", - "sha256:40af7ebe53c7990e110dc4ad03566b12c3ac996254298a3d39046dd69cfcb2c2", - "sha256:44eca38755d0105bb32f47d085f5dd449846a449e1245fc105889e3279dcf8e3", - "sha256:46fbbec4e4fab7428d4396a3823f9320e4560aa3113b89eeebce712c27c9ed5a", - "sha256:4714c70067a08b604d0bf3bc4dfdf82e52944afab41d0428d460862763d2f79b", - "sha256:49a33ded29b0b2fa7a367a02cf0fb89af602bb87542a16177ec8ce1c9c51d12a", - "sha256:4acfc34bd4d3c58754fc9f22ff1b5e92aabce68f3d4bf7b71a0b732d9bceb78a", - "sha256:4d6a998191f5ebe3b8c28463ff72bc030250008b3193c402464efadd08b5ca02", - "sha256:4f770846edae8f00ecc57af825bce811f787f87a7dcf0e90d191790efe5b31f7", - "sha256:514db9a79337068981ee2137310283a07b4b885c584991097a91a4da419bcb81", - "sha256:540632bf882ff8fc88f2e1697be0761578e89e0d79fb4a8a6d65dc5da7e729d4", - "sha256:54bf3522d6f7351e55f89a62d5c2bf138ad557b031670266c5df604ae88e0b5a", - "sha256:57ea07d28695a7a40304d42251892a8df765e5588c10ee32afeddcd5df33c0a2", - "sha256:5a2e7ca615c3ddc15b82687e05a624e5f5cba3f1d6c20cb81172d70ea498451e", - "sha256:5ba10966d4f03dd96a14365be4b8e37c327c76f11c3ca867116966cdd9f98066", - "sha256:5cbd50e6a50d6b99283a826b18cbdebf65b0797689a7535cb0e9dd37be0f63c3", - "sha256:5e4646e9a6af29af354204011bf5769cb0276ec5b64653e42f90b3e13845169f", - "sha256:5f1c5be60add78fabb4aacd13c5a348ae79d2fcbfc7fa78da8f1eb192273b370", - "sha256:610d68800435903e303ca0542b9d3e4eb72a12ff33a6d471a070c1d81eebd3c2", - "sha256:6199707cc40e0e9cd39c36fbc97bec416c704e1d0ddce03412bb3b3e6a90ccd0", - "sha256:6281aecdf2732940f4fe06bd6adec5ae4d59b78b080b8e3a6b81467301010988", - "sha256:63e38be0d75a654deaa06be32fb4cab883a4222940be1d05861b6717679cbadb", - "sha256:666c7c5036df57b693026398b69b41874a1931ac5b3485fd910e57bfac253869", - "sha256:667b881d083ccae3900ea5a241e17e5007ca78844c53ed389bb63d48f729d9c7", - "sha256:692e409052e7436029bbb32977cd7c5bf806ac5fa4085b973996785ffadad33c", - "sha256:6a5f3532125233c261cf61f32df4059cfcf482eb793c7d3db8452e3142028b86", - "sha256:6aa1a40f9cbb3da9f80714c5966b8946c21e6a2530d809b9498b33161e3c8733", - "sha256:6c79a044cacf360ec46738d863d2f41c9300d2a06ef4a7402ea0df306a350e61", - "sha256:6eb63b1417efaf7d1002a6ad034a40d44376afcc16508a57f8e74b49ad26a095", - "sha256:70ea956f6cc4a37620966b56c2e205d88ca3e6d85ec063277e414b1035cddad3", - "sha256:71b2604c9bfc1b115547d63a094d5244b3f02799833513a99a68aaa7b167c4cb", - "sha256:78d6f9286a629ce52728430afe18f8ed2b6c39a1fddb3802d7244b9983910ad2", - "sha256:7a3fc4358e65826c515350f199c210de747cf669998211b1ee6c2e46de364b24", - "sha256:7b33e751cab03fdc960095b1e326cb5a03f5ee577d6ded59f3d1c100f8668882", - "sha256:85e0675f47be4eff0636bf88c02140ea89168ae0df3ff1f3f464e9de9610d277", - "sha256:860a86bc2c80237f5dff52edcf427e10a8d8352271fd84845429a3e60199e02c", - "sha256:884a4edbdad77be9d0ef36142c8b504351b170df0bf62b51e784fadabf311c42", - "sha256:89ed35666c95d3efe1955056afcde09e62a57a34e2a4398b17f9f6c1564f0b25", - "sha256:8b93618102caf12801638a01a2b478a55410ddd71bd41cfaf6f707953a49ac43", - "sha256:8fcaef74d2ab0f607d7ff85a0d15e21bb5a258c4a58df1908396eb50d7f4ed3c", - "sha256:95f5217e76a046b9f228a101717ef8d42b1eb3d9d196d15202db5bf41df88936", - "sha256:9dc203d6ce6b9106d54e2a93f41dfdfebfbca2d99962ba503bfd3e5921a6549e", - "sha256:9e19d17ab02bf16832a2c8c0d55a486792c5b1645665652ee9531aebcc30cb72", - "sha256:9f3a96b6d39a4872222beee72e1df41d2ff886ae96152cf3e757ef8c5673ef0e", - "sha256:a071be341c2bd9b0188e62d173509f024e0a35b1c342c53c50f8daaeda8c3bd8", - "sha256:a150c0875ac8fd87f1c398650841308a30d65facf7416b12dbdb9cfdcbe5a48c", - "sha256:a1d209375c503472b3c0a340cdf3c55fcd82e84b46dda7caeaced59faba373ec", - "sha256:a8d93334d4961c9d566b1f046c81dee475b7c21eb730728d38237bfa70d1c8e6", - "sha256:acdb400538cf4769543548bb5d1eb23d39bed4f96554a6078cb728c7cb2c268b", - "sha256:acf1581c4f21ed4b80a2dded504d87b055a071a84d5737ea966435f768275ac6", - "sha256:b0a5747586d4467efd1f932710b269131c9717a872dce082cd92a00c1c13123a", - "sha256:b27d89af91a555f58e08e4902dbcbc48862fd40095720ca705990476bd93b7ac", - "sha256:b29518c9c2ec7e373e68259206a137c7f4f5439c58baaec4b5ab3ab799850a4e", - "sha256:b4141a3e5342ee3053a9cab54d25b64ed28289c1041e4c54b3d99839314d90ce", - "sha256:b5314743ebe926c2fda35d0a298c565c885505f6635c2a30936363404cf274a7", - "sha256:b584dfe615d151e9b8f0a8ecb3aee6147f2927ec5b95ba25fe621f5377510928", - "sha256:b62af5a8cc96a194eaa01a9ed7b34a3ffa58d3d8daaa1a0d7a749353ad12d228", - "sha256:c20b9ad156a79eb97be5cf9e069eec01d2f0dc8472ffbd75299a8b2d4c2cbbde", - "sha256:c21ca9a1c63d4509158f478aeb9d02914dcc52adc68d1bc9dee2452284ee5996", - "sha256:c452d17eeb95d563fc8b936f3050301dbd1d268126c4632d8b70ede9696202ee", - "sha256:c5492b9929826e07cc3fcb9739ae87aab05dff6b5e67a9b73fd1700c6d008981", - "sha256:cb6c657104393b5fbff01a5f59b2023db74058a8077d94475d6c25d03882a108", - "sha256:cc3c3e12cdaeb92d7dcf13db00e9f6b1956b910e47256e696df1cfa946d02159", - "sha256:d1467d1e7b48a73ca7237e0ee4335f3d02b923dbc27b82fd254bc301c97d4026", - "sha256:d336820adbb914debbc90a1d8c1bfc4bea55996aecf64866a989d35d1f9fd903", - "sha256:d33e61021222ce7f9792bcac870d6f58d8adfceda33ab857b01264f4560f2c5f", - "sha256:d488e6e9d3bb8ba5ae7066d5be885ae9670eba021b8c6ccb9a3a568e6b19d6e5", - "sha256:d925fba0c14d5b498a8028b0107beebdfd16c5d48d702ff54f879cb017aaaca3", - "sha256:dbec68ce61b64cb73cab4d33df9433427b1713c8bcccb181dce695c1b6f8e87c", - "sha256:e03abdaa17d553f17e1d1d06bb266b3970106c78051d06795723e748d8e49d11", - "sha256:e30871b2d58996cb81aac52d2b1d15ac05257131ef0f90f18c2115a380fbfe7c", - "sha256:e4c01b0bfc6209590960e68eac083cd22d5d87c21f974dd6208cafa5d3542bc8", - "sha256:ea3b9806c89f61da22fddf1f12dd524fb368e5e28f1261fbdafe5c3cd8ce893b", - "sha256:ed94a81506e3d1bdbad5108f497a58f2a2354aedb4ca314d5326f07d1fd1ac2d", - "sha256:edc01ea4e1ec5a1649a28866262bf24195889ff7b27bdd947029a6086741de9b", - "sha256:f0b7b8bbbec3ce9467ee0ebe334622fd90624f593edd3136c567811453fc4fae", - "sha256:f12eb7896e81caf403a2b18c9406426f1207361e7239c057ab29c076d4257e83", - "sha256:f13087e06f68fea4941c21a0c541c00553aa16e4f8fd7bbe2b198df761e964d6", - "sha256:f4d2038c64f36df96cfd3fa0937910e231eafbf897e70a06c155a817bb632fa6", - "sha256:f79bfd2847513a7ac801bbafd1de02348a37926ac439eeb4bfe96fcff4eada15", - "sha256:ff82be7f1ef73634cb77890a770743239bc3d487b848669be1c599889336dc0a" + "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5", + "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", + "sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521", + "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", + "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", + "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", + "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", + "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", + "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f", + "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee", + "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", + "sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb", + "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", + "sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05", + "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", + "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", + "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", + "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", + "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", + "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", + "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", + "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", + "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", + "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf", + "sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6", + "sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df", + "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", + "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126", + "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", + "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", + "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", + "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", + "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", + "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", + "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", + "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", + "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", + "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b", + "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621", + "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", + "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", + "sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491", + "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", + "sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d", + "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", + "sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42", + "sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c", + "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", + "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", + "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8", + "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", + "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", + "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", + "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", + "sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966", + "sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192", + "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", + "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", + "sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b", + "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", + "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6", + "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573", + "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af", + "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", + "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", + "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", + "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", + "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", + "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", + "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", + "sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e", + "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", + "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", + "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", + "sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe", + "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", + "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", + "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", + "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", + "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", + "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", + "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", + "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2", + "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", + "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", + "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", + "sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3", + "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", + "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3", + "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", + "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730", + "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", + "sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd", + "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", + "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", + "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85", + "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", + "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", + "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", + "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", + "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", + "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", + "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", + "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04", + "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8", + "sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce", + "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", + "sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505", + "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", + "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", + "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", + "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", + "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", + "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", + "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", + "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", + "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7", + "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", + "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3" ], "markers": "python_version >= '3.10'", - "version": "==3.14.0" + "version": "==3.14.1" }, "aiosignal": { "hashes": [ @@ -186,11 +186,11 @@ }, "anthropic": { "hashes": [ - "sha256:d0e4a7448e54c3942833cee5b3de5f1b31289fd49999bfbcc2ec0c0acaddf75f", - "sha256:f26e2645e31f66eff526b923f539b80b4b6eda1a918790cd77c0afe5e24a2203" + "sha256:83e06b3d9d40ff5898f588020e0cc4e42187de954549a3b5fbe6e2685a09c785", + "sha256:ce7d94a7657f2aa29338cca448945eac621b4f62c1794cf461cb32847223e9b8" ], "markers": "python_version >= '3.9'", - "version": "==0.106.0" + "version": "==0.109.1" }, "anyio": { "hashes": [ @@ -459,58 +459,55 @@ }, "cryptography": { "hashes": [ - "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", - "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", - "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", - "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", - "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", - "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", - "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", - "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", - "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", - "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", - "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", - "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", - "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", - "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", - "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", - "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", - "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", - "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", - "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", - "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", - "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", - "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", - "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", - "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", - "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", - "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", - "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", - "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", - "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", - "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", - "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", - "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", - "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", - "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", - "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", - "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", - "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", - "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", - "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", - "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", - "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", - "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", - "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", - "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", - "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", - "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", - "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", - "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", - "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b" + "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", + "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", + "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", + "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", + "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", + "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", + "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", + "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", + "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", + "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", + "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", + "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", + "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", + "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", + "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", + "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", + "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", + "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", + "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", + "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", + "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", + "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", + "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", + "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", + "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", + "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", + "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", + "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", + "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", + "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", + "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", + "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", + "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", + "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", + "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", + "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", + "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", + "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", + "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", + "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", + "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", + "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", + "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", + "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", + "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", + "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b" ], "markers": "python_version >= '3.9' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==48.0.0" + "version": "==49.0.0" }, "distro": { "hashes": [ @@ -710,11 +707,11 @@ "requests" ], "hashes": [ - "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68", - "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c" + "sha256:130f6fd5e3f497fdad897a23ed9489973437edf561238c4b92a4d02c435f8af9", + "sha256:784e9837f92244141250470d47c893df50cbab485ce491aca5e9deb558ad2b48" ], "markers": "python_version >= '3.10'", - "version": "==2.53.0" + "version": "==2.54.0" }, "google-genai": { "hashes": [ @@ -820,60 +817,60 @@ }, "grpcio": { "hashes": [ - "sha256:0fba53cb96004b2b7fb758b46b2288cb49d0b658316a4e73f3ef67230616ee65", - "sha256:194eddfacc84d80f50512e9fd4ee851d5f2499f18f299c95aa8fb4748f0537e0", - "sha256:19f201da7b4e5c0559198abe5a97157e726f3abe6e8f5e832d4a50740f6dcc22", - "sha256:21ec30b9ea320c8207ea7cd05873ad64aa69fdd0e81b6758b3347983ba20b50a", - "sha256:275144b0115353339dbb8a6f28a9cf8997b5bf40e37f8f66ac0b0ea57e95b43f", - "sha256:300f3337b6425fd16ead9a4f9b2ac25801acb64aa5bc0b99eb69901645b2b1d2", - "sha256:3755c9669307cad18e7e009860fdea98118978d2300451bd8530a53048e741e7", - "sha256:3d4e0ce5a40a998cf608c8ba60ecfe18fdf364a9aa193ae4ac3faeecd0e86757", - "sha256:40edffb4ec3689373825d367c4457727047a6e554f03245265ecc8cc03215f22", - "sha256:43c121e135ae44d1559b430db2b2dfad7421cbbe40e1deba506c7dc62b439719", - "sha256:4e032feb3bfb4e2749b140a2302a6baa8ead1b9781ff5cf7094e4402b5e9372e", - "sha256:5192857589f223e5a98ff0e31f6e551b19040e647d17bfe10116c8a2ce3b8696", - "sha256:57b3b0e73a518fa286959b40c3eddd02703504ca186e8b7b2945954519bd8b2c", - "sha256:5e925a70fe99fe5794f7beca0ea034c75f068afcc356d79047e73f99cdcca34c", - "sha256:62bbe463c9f0f2ff24e31bd25f8dd8b4bae78900e315915a3195a0ef1471a855", - "sha256:638ccc1b86f7540170a169cb900799b9296a1381e47879ce60b0de9d3db73d33", - "sha256:725801c7086d7e4cd160e42bb2f54e0aeb976b9568df3cc6f843b15d29b79fb1", - "sha256:77eb4e9fe61486bd1198cc7236ebb0f70e66234e63c0348f40bc2553ed16a88b", - "sha256:7915a2e63acdc05264a206e1bddfd8e1fb8a29e406c18d72d30f8c124e021374", - "sha256:794e6aa648e8df47d8f908dc8c3b42347d04ec58438f1dcd4e445f09b4f6b0ce", - "sha256:8226ba097eed660ef14d36c6a69b85038552bb8b6d17b44a5aa6f9abf48b8e08", - "sha256:87e33b7afcfb3585121b5f007d2c52b8c534104d18f556e840d35193ca2a9141", - "sha256:8bb1789c94322a13336a2b6c58d9c14d68f8628b6e24205a799c69f5bf8516ce", - "sha256:8c0855a350886f713b9e458e2a10d208009dcaa849f574e39cd6067db1fe1279", - "sha256:909bb3222b53235498d2c5817a0596d82b0aaea490ba93fdf1b060e2938a543c", - "sha256:97bbd623f7ded558fd4f7cb5a4f600c4d4de65c5dd364c83a5b14b2a10a2d3b5", - "sha256:98c6240f563178fc5877bd50e6ff274463e53e1472128f4110742450739659fa", - "sha256:9f355384e5543ab77a755a7085225ecc19f32b76032e851cbd8145715d79dec8", - "sha256:a524cd530900bd24511fcb7f2ed144da4ea37711c4b094475d0bceca7a93a170", - "sha256:a5acd7efd3b1fe9b4eb0bcaaa1507eed68a0ad0678b654c3f7b464df9ba9dca5", - "sha256:a9351055f52660b58f3d4890ea66188b5134399f82b11aa0c55bd4b99eff5390", - "sha256:aa948712c8e5fa40ec250870bda14bc7578e1bb832a8912d9d2a0f720518edbe", - "sha256:aaaa4f7f2057d795952e4eacf3f342be8b5b156992f6ac85023c8b98794ebd47", - "sha256:b4108e5d9d0f651b7eea749116181fe6c315b145661a80ec31f05ec2dbe21af7", - "sha256:b76ea9d55cd08fcdbda25d28e0f76679536710acb7fbd5b1f70cb4ac49317265", - "sha256:b8b025b6af43ee0ad4a70307025d77bcab5adde7c4597786010d802c203e9fc5", - "sha256:b93cee313cae4e113fbb3a0ce1ea5633db6f63cfde2b2dc1d817429026b2a50b", - "sha256:c197e2ef75a442528072b29e9755da299110e8610e8bcbb59a6b4cf55384f005", - "sha256:c36f5d5e97944cbda2d4096b4ae262e6e68506246b61582acf1b8591607f3ccc", - "sha256:c4fe218c5a35e1d87a5a26544237f1fa41dfd9cbd3c856b0810a30061f8b0aaf", - "sha256:c6ff087cb1f563f47b504b4e29e684129fc5ae4863faf3ebca08a327764ee6cb", - "sha256:cd78145b7f7784661c524624f3526c9c6f891b30a4b54cb93a40806d0d0d61e9", - "sha256:db217c2e52931719f9937bd12082cd4d7b495b35803d5760686975c285924bf8", - "sha256:dbdb99986548a7e87f8343805ef315fd4eb50ffaabf4fb1206e42f2542bb805d", - "sha256:e4d053900a0d24b75d7521139a3872150301b3d6bde3bed5e12318fb25791e4d", - "sha256:e7746ba3e6efc9e2b748eff59470a2b8684d5a9ec607c6580bcaa5be175820bc", - "sha256:f345de40ef2e65f63645d53d251824e6070e07804827c5b00ec2e44555f9f901", - "sha256:f750a091fff3a3991731abc1f818bdc64874bb3528162732cb4d45f2e07821a6", - "sha256:f85570a016d794c29b1e76cf22f67af4486ddbe779e0f30674f138fa4e1769ec", - "sha256:fbbe81314a9d92156abce8b62c09364eb8bafc0ca2a19919a45ec64b5c6cb664", - "sha256:ff83d889e3ebf6341c8c7864ad8031591ad5ca61599072fc511644d1eb962d2b" + "sha256:0490c30c261eded63f3f354979f9dc4502a9fb944cccb60cd9dc85f5a7349854", + "sha256:0a37165cc80b1a368384b383e63a4c38116a10467ae44c904d2d7468c4470ec2", + "sha256:12b7524c88d4026d3dcb7b0ebe16b6714f3b4af402ddd0f0639ab064a00c87c3", + "sha256:15641444eca4a29358107b3dceb74c1c6305c55c822fd199b458aaea4068a7fb", + "sha256:1b22c80559854b789a01fd89e8929b3798a156c0829b5282a8939f33ad4115ad", + "sha256:1e123f9b37edb8375fd74130d1f69c944bbf0a7b06761ae7211154b8759e94d2", + "sha256:24c8e57504c8f45b237e40b99262d181071e5099a07053695b75d97bb53053a0", + "sha256:2c2e2ae6867c2966b8daccc836d54a13218e0007e9a490aeb81dd05be64d22d7", + "sha256:30e825f6848d9f18bba350ed6c75c1b02a0b5184474a31db9a32b1fa66fd8c79", + "sha256:3768a5ff1b2125e6f552e561b6b2dca0e64982d8949689b4df145cf8b98d7821", + "sha256:3ad74f8bb1a18963914c5452d289422830b39459e8776ebbcd207be1fbfb1d94", + "sha256:410482da976329fe5f4067270401b12cf2bd552ff8020f054ecfaddb5475f9d6", + "sha256:428bec0161b48d8cf583c068591bc0016d0d9cfff52462b72b3884861ea768c5", + "sha256:506f48f2f9c29b143fca3dad7b0d518c188b6c9648c75a2ae6e2d9f2c13a060b", + "sha256:58ad1131c300d3c9b933802b3cc4dc69d380822935ba50b28703156ea826fbf7", + "sha256:592b5fee597faa91cce2dd294dd7d9a1c83d76c4dbf877e33ec1adb866b2fbed", + "sha256:61233fe8951e5c85dff81c2458b6528624760166946b5b47ea150a589168411f", + "sha256:62481553b1793a27e9b9c3cf9e5bd483ef045ca72462592074b46d42b0c4d9b9", + "sha256:6282caffb41ec326d4cb67ca9cf53b739d1b2f975a2acb498c7418e9f7d9a416", + "sha256:69ef28e54fc85397f91b8c19592b8ef3d81952080366914823bd8572a2958120", + "sha256:6f9a0c9c1cc15c112d1c053064fd032b64917062292c3d70aea280e02ae10b77", + "sha256:6fa10a767143a5e82e8eaab53918af0cd8909a57a27f8cb2288b80a613ac671b", + "sha256:766bc7c9a9c340342f4c864ccbda8e78111e4751f13b895812b9c148fb79e9d0", + "sha256:78e29211f26da2fdd0e9c6d2b79f489476140cf7029b6a64808ade7ca4156a42", + "sha256:819edbdcb42ab8598b494bcf0222684bbb7a3c772bd1b1f0be7e029a6063c28e", + "sha256:85b10a45b8993d195c4f3ff57025b8d1e11834909ee475c403bfa60cb4caefaf", + "sha256:88268ca418cacea64cecb0d1d600d3c6b3a8038fcba02e1e205178c5b1f47661", + "sha256:8b39472beafc0bdcafc4c8c73ad082ebfdb449d566897a61e7acb4fa88089115", + "sha256:8ea1936c26b99999b27479853039a7f34713f56c49375ad52b38535ec93a796c", + "sha256:98a07f9bf591e3a8919797bee1c53f026ba4acd587e5a4404c8e57c9ec36b2a5", + "sha256:a185a04039df6cae8648bc8ab6d6fde7bf94f7188ecf7828e76ac52eef1e41d6", + "sha256:a35009284d0d3d5c2c9601c164a911b8b4331608d98a9a66d47d97bb2f522b70", + "sha256:a3acb384427816dd5d470f47e62137b87f74da694faa8a50147012cf40df276a", + "sha256:aa2ba7d2ad6df4d80127cea65e5b8d5e2c3adbf153ff4804452836328aca7c54", + "sha256:b10e1ff4756ed27d5a29d7fc79cfce7ef1ff56ad20025b89bac7cf79e09abbbe", + "sha256:b137f4bf3ada9dc44d411478decc6ff09a79ed30b306cd2abaa98408c3588137", + "sha256:b259a04a737cb3496be0901328eb8b7552ed8df4865d8c8f1cf1bffcfc0776a3", + "sha256:b427c19380991a4eaab2f6144b64b99b412043314c6bf4ab544f97bb31ee4190", + "sha256:bb693b1e3d9a2f3fd228e2110daf4b5aeedb36761ca1e4282f74725f6d89f611", + "sha256:c261d74b1a945cf895a9d6eccd1685a8e837531beaab782da4d630a8d12deffb", + "sha256:c5bf2dc311127d91230cc79b92188c082634a06cf66c5234db49a43b910183b0", + "sha256:ca1cc11d82677b9662082e5478b7528e2b7db7beaa6bdff42bd62789d81be399", + "sha256:d4b2dddfc219f54f956ccd53cf76a1d338ffe68fc7f2849ec9c7feb9927ff692", + "sha256:d71d30f2d92f67d944631c523713934fee37292469e182ebcd2c1dd8a64ce53f", + "sha256:d865db4a6318e1c1bea83292e0ed231090538fc4ca45425b0f0480eb338bbc6e", + "sha256:e2aa72e3ce1770317ef534f63d397b55e130725f5149bd36077c3b539019db27", + "sha256:e3657301562ac3cb8018d30d0d3ebfa39932239f7b5703422057ef14b69949f5", + "sha256:e64dd101d380a115cc5a0c7856788adb535f1a4e21fc543775602f8be95180ae", + "sha256:e8ca6a1fcdb2943c9cbc1804a1baf3acb6071d72a471591678ded84218006e14", + "sha256:edb59506291b647a30884b1d51a599d605f40b20af4a7dc3d33786a47a31de60", + "sha256:f9a0ebbe45c29b5e5866593c12b78bd9035f0f0f0d4bc8361680cd580d99db49" ], "markers": "python_version >= '3.10'", - "version": "==1.81.0" + "version": "==1.81.1" }, "gunicorn": { "hashes": [ @@ -1076,21 +1073,21 @@ }, "langchain": { "hashes": [ - "sha256:d6e0654c22848925534f5c0a706f9be481bb09a619ec60a738fbd1e5502e457a", - "sha256:e51b05ab23d056bc6bf2d97d8c694fb92d6d5765126fef74565d007c27581672" + "sha256:4af49ad1095799e4408b489fb79d4b8b49292453618b202d8a697fca59bb6871", + "sha256:9b14ef0db9ef314299ded858b22ca2a40b8f1b05c8c9cb6b82d53a53075fef00" ], "index": "pypi", "markers": "python_full_version >= '3.10.0' and python_full_version < '4.0.0'", - "version": "==1.3.4" + "version": "==1.3.9" }, "langchain-anthropic": { "hashes": [ - "sha256:10a5d2915e8d4fd8a9f169774e760b089c7f14de044e13259db5f1c2d3d59d9b", - "sha256:9ea39ed345f8b13f13bb37549130eedcec2d032eb08b4942642e758c5ba3ece5" + "sha256:78942d4458d883b7d362438a095ed501ed84f44d402622404482481fc973b9da", + "sha256:dbd412a956b6b8b0716d9d8460ef71f834a6731cdbfc59e6160482a4a9fb5200" ], "index": "pypi", "markers": "python_full_version >= '3.10.0' and python_full_version < '4.0.0'", - "version": "==1.4.4" + "version": "==1.4.6" }, "langchain-classic": { "hashes": [ @@ -1111,47 +1108,47 @@ }, "langchain-core": { "hashes": [ - "sha256:8234eb8cd3200f690e278159b7d7cee5976381ec90ece7b48db8d8e8850ab37d", - "sha256:e5dee06e70c123cb98cb0158e4416efac1e386ff47a484901ccf88555e28eec6" + "sha256:7a825d77de0a3f39adbd9d09612a75e85527e14a52c1601089bcc062972d9f2b", + "sha256:bcadd51951140ecdcba98311dbd931ba5de02a5ba8a2288dad5069c1eea2a13d" ], "index": "pypi", "markers": "python_full_version >= '3.10.0' and python_full_version < '4.0.0'", - "version": "==1.4.1" + "version": "==1.4.7" }, "langchain-google-genai": { "hashes": [ - "sha256:0e2c1021a15c91e60b68d813bb3e793bd1d9396b3f8639b943ab4e56e5652e04", - "sha256:2f5de7a8a6552ffb64b907aca7503fd5e34d1a3240e280abcdc5f7eef480edd5" + "sha256:289699ddb8e1076a76144f83e25e0086e4ce629b196fc103251f2a629e0756e5", + "sha256:2abab4be22699a9cc29948b2bf012946f51a0bbf10ab3a4a9a129047234829f8" ], "index": "pypi", "markers": "python_full_version >= '3.10.0' and python_full_version < '4.0.0'", - "version": "==4.2.4" + "version": "==4.2.5" }, "langchain-openai": { "hashes": [ - "sha256:7da39a3c70cbafa93853456199e39a264dc70651be79b12ac49b4f6a448bce2d", - "sha256:8698ffcee9a086e91ab6d207f0026181a03effcbf86bf9aee1808ee35af69dcc" + "sha256:240917ae88d754b389a6f2ae06fa262c50c094eb4f576c27d560dff6b86c2f62", + "sha256:3d247f43bba9f85d32a374b1bdf3932a0d1e3c60913ebeadf68630de52add67e" ], "index": "pypi", "markers": "python_full_version >= '3.10.0' and python_full_version < '4.0.0'", - "version": "==1.2.2" + "version": "==1.3.2" }, "langchain-perplexity": { "hashes": [ - "sha256:b806e62ccabc5dfc385c7afeee05a48b32c2553505227e92807053389e8f70c9", - "sha256:ba9424c0447906704a6d1a49003689e29480291c8716311934ab305990dd0fc3" + "sha256:bfde8a61326257a47a35697908ae7f72ab49d6d887894d1da0e08007b6182602", + "sha256:c39458651dfbb5d9925275ba119050d5660b0946ecfa2aa770eeda298d83ed71" ], "index": "pypi", "markers": "python_full_version >= '3.10.0' and python_full_version < '4.0.0'", - "version": "==1.3.2" + "version": "==1.4.0" }, "langchain-protocol": { "hashes": [ - "sha256:3658c142c5d0fb3a023a4be442ce4c15c6d626aab6135eb79a76dc64ad19c3c3", - "sha256:806c7cdd951b1c4f692fa40fce60821ff0f221d4360e27673ddf2c2b99c2b7ff" + "sha256:982a08fe152586ed10d4ff3d538c2e0b5766e5f307cdea325e10be3f2c17cae6", + "sha256:e7cbe58c205df4b4fd87dc6d5bb23f10e13b236d0e2e1b0b9d05bc2b648f3eea" ], "markers": "python_full_version >= '3.10.0' and python_full_version < '4.0.0'", - "version": "==0.0.16" + "version": "==0.0.17" }, "langchain-text-splitters": { "hashes": [ @@ -1172,11 +1169,11 @@ }, "langgraph": { "hashes": [ - "sha256:5df076973a2d23efb13eceb279d1e5b46feebcbbeded0a86a2ef669abd9e4399", - "sha256:ffe3e1e31dce28907640f82525858470f293506d2b272d07ea3b3ce97974b067" + "sha256:09a3bdec6fdb3228623fc78b6f69a1400d383f66348d0b04d0efb692022cc6ef", + "sha256:9286bb5def82fc865959c14378fe473518dc097d586225f622f029637a2a4bb9" ], "markers": "python_version >= '3.10'", - "version": "==1.2.4" + "version": "==1.2.5" }, "langgraph-checkpoint": { "hashes": [ @@ -1204,11 +1201,11 @@ }, "langsmith": { "hashes": [ - "sha256:c9519cabc75568d088df045710d1b86eae9780c91054528b2aa7e6cb1fc80c52", - "sha256:f16e37fcd5a8a2d4db30eae0e399a866a65ce5cc86218825c59409ed57a3bf53" + "sha256:72e57c79eef82ddbbe22c5edb8bdf79eb557e65c3a1172ddfa05a724555c8294", + "sha256:9cdf26495814c9ae38998cee7895cdba267b0b3566b598e407c0bb7eefb5aaf9" ], "markers": "python_version >= '3.10'", - "version": "==0.8.9" + "version": "==0.8.15" }, "lxml": { "hashes": [ @@ -1685,11 +1682,11 @@ }, "openai": { "hashes": [ - "sha256:20cc7952e8501c7e5773dd2ef7be437bae9cb549044902e1041a83a54516e375", - "sha256:db5c362acd6604b84f076abbefa66826ea4b46ecba2954ed866e6a149a1352c0" + "sha256:23d617a0432457ad844973bee8f540be9da90894f7c5686852d2d365da058f57", + "sha256:a939565f350cb7443cb843b801b88c716ac8024b492fb94ca269d5f6b1bbefd6" ], "markers": "python_version >= '3.9'", - "version": "==2.41.0" + "version": "==2.41.1" }, "opentelemetry-api": { "hashes": [ @@ -1861,11 +1858,11 @@ }, "perplexityai": { "hashes": [ - "sha256:94dcf1dbd69eac2edacf3115b5e63a2fac008f419ff47a3118f1601659fc71bf", - "sha256:9818941ac3b40529b8ea34b968d898a8b47a63d815e0fae3add5f8c8dc35f15e" + "sha256:580a000bcd7b78becb90e04863e365f06cbe0687a76509fee014e87a12fcae89", + "sha256:fdfd8047415d1a7e647946d065e32fb88ef91c7553c8c283a38a83753f089ba2" ], "markers": "python_version >= '3.9'", - "version": "==0.37.0" + "version": "==0.38.0" }, "pillow": { "hashes": [ @@ -2958,11 +2955,11 @@ }, "tqdm": { "hashes": [ - "sha256:fc163d96b287bd031e1aa24421ce4411b25559bd0a1be4fe649bdaa4d2c02bf5", - "sha256:fea4a90e4023f764914569f7802a297277c5ab1a66be5144143e142e1a4031d8" + "sha256:89c230e8dbc67c7615c142487111222f878c77427ea09549960f62389e258add", + "sha256:d4240441fb5353290b87d6a85968c9decc131a99b8c7faa28269d829de669ede" ], "markers": "python_version >= '3.7'", - "version": "==4.68.1" + "version": "==4.68.2" }, "typing-extensions": { "hashes": [ @@ -3194,12 +3191,12 @@ }, "xai-sdk": { "hashes": [ - "sha256:6508b702d01da9c55c15cdcb329c4ad58eb9251340b2514d0974bcba11e764e3", - "sha256:de1dcb856941bcbc64c0e61b7202395593c1950e0e00e6aa0ca7657448c03aca" + "sha256:88a6a53181fa13d55662e3296dafd2a3cf258803450b7d82c7318b8c64e8a485", + "sha256:ec695ad8b459a4080c01f6e7bb8abf38ed6a4ec69532bfdd73c4de14db1d485c" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==1.15.0" + "version": "==1.17.0" }, "xxhash": { "hashes": [ @@ -3968,28 +3965,28 @@ }, "ruff": { "hashes": [ - "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", - "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", - "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", - "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", - "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", - "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", - "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", - "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", - "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", - "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", - "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", - "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", - "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", - "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", - "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", - "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", - "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", - "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4" + "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" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.15.16" + "version": "==0.15.17" }, "the-agent": { "editable": true, From 0d0d7d7b6033cb0f274a03cafdb1e827fbcd69c9 Mon Sep 17 00:00:00 2001 From: the-agent-abot Date: Sat, 13 Jun 2026 08:15:16 +0000 Subject: [PATCH 2/7] Auto-lock: Update dependencies --- Pipfile.lock | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 269d326d..94f49bc3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -210,11 +210,11 @@ }, "beautifulsoup4": { "hashes": [ - "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", - "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86" + "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", + "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9" ], "markers": "python_full_version >= '3.7.0'", - "version": "==4.14.3" + "version": "==4.15.0" }, "certifi": { "hashes": [ @@ -1091,11 +1091,11 @@ }, "langchain-classic": { "hashes": [ - "sha256:d9d9be38f7aa534ed0259c2410432e34a1f80b1d491e686749bb55af56479be3", - "sha256:debbec8065e69b95108d2652e8d5c44f4516e19aa8d716c02ed2211c3aee099d" + "sha256:1a11ea7fbe630c4f2af2f3873d27718ceac9488cf32d0821030be7cf039a6213", + "sha256:ada0cc341a8a5b80fb24d73bdfaaeb849056ee2d8a41cc468355163fd3667484" ], "markers": "python_full_version >= '3.10.0' and python_full_version < '4.0.0'", - "version": "==1.0.7" + "version": "==1.0.8" }, "langchain-community": { "hashes": [ @@ -2876,11 +2876,11 @@ }, "starlette": { "hashes": [ - "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89", - "sha256:9b9b5ebb992e67d6093741e63c2f59e4f6fff986f81163c087867bd7b924b3f6" + "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", + "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6" ], "markers": "python_version >= '3.10'", - "version": "==1.2.1" + "version": "==1.3.1" }, "tenacity": { "hashes": [ @@ -3761,18 +3761,18 @@ }, "distlib": { "hashes": [ - "sha256:9c2c552c68cbadc619f2d0ed3a69e27c351a3f4c9baa9ffb7df9e9cdc3d19a97", - "sha256:c3804d0d2d4b5fcd44036eb860cb6660485fcdf5c2aba53dc324d805837ea65b" + "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", + "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed" ], - "version": "==0.4.1" + "version": "==0.4.3" }, "filelock": { "hashes": [ - "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b", - "sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e" + "sha256:7fc1b3f39cf172fd8203812043c57b8a65aef9969f38b6704f628b881f761a84", + "sha256:e58333029cc9b925f39aad59b1d8f0a1ad836af4e60d7217f4a4dba87461261d" ], "markers": "python_version >= '3.10'", - "version": "==3.29.1" + "version": "==3.29.3" }, "identify": { "hashes": [ @@ -3860,11 +3860,11 @@ }, "python-discovery": { "hashes": [ - "sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da", - "sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3" + "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", + "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690" ], "markers": "python_version >= '3.8'", - "version": "==1.4.0" + "version": "==1.4.2" }, "pyyaml": { "hashes": [ @@ -4003,11 +4003,11 @@ }, "virtualenv": { "hashes": [ - "sha256:38e6ee0a555615c0ea9da2ac7e9998fe8dc3b911dd33ad8eaad2020957653b0c", - "sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae" + "sha256:75f4127d4067397c64f38579ce918fec6bf9ca2cd4f48685e82952cc3c035840", + "sha256:938ff0fd3f4e0f0d3a025f67a3d2f25e3c3aabbcd5857ea6170619138d72d141" ], "markers": "python_version >= '3.8'", - "version": "==21.4.2" + "version": "==21.4.3" } } } From 0174261b72ac8194c690f897714ad16ed7dafed9 Mon Sep 17 00:00:00 2001 From: Milos Marinkovic Date: Tue, 16 Jun 2026 14:33:28 +0200 Subject: [PATCH 3/7] Add the new ChatConfig repo and domain models --- src/di/di.py | 10 + src/features/chat/config/chat_config.py | 19 ++ .../chat/config/chat_config_mapper.py | 73 ++++++ .../chat/config/chat_config_remote_data.py | 12 + src/features/chat/config/chat_config_repo.py | 92 ++++++++ test/db/sql_util.py | 6 + .../chat/config/test_chat_config_mapper.py | 155 ++++++++++++ .../chat/config/test_chat_config_repo.py | 223 ++++++++++++++++++ 8 files changed, 590 insertions(+) create mode 100644 src/features/chat/config/chat_config.py create mode 100644 src/features/chat/config/chat_config_mapper.py create mode 100644 src/features/chat/config/chat_config_remote_data.py create mode 100644 src/features/chat/config/chat_config_repo.py create mode 100644 test/features/chat/config/test_chat_config_mapper.py create mode 100644 test/features/chat/config/test_chat_config_repo.py diff --git a/src/di/di.py b/src/di/di.py index 65e28b38..bcd7e4d9 100644 --- a/src/di/di.py +++ b/src/di/di.py @@ -57,6 +57,7 @@ from features.chat.chat_image_edit_service import ChatImageEditService from features.chat.chat_progress_notifier import ChatProgressNotifier from features.chat.command_processor import CommandProcessor + from features.chat.config.chat_config_repo import ChatConfigRepository from features.chat.currency_alert_service import CurrencyAlertService from features.chat.dev_announcements_service import DevAnnouncementsService from features.chat.llm_tools.llm_tool_library import LLMToolLibrary @@ -114,6 +115,7 @@ class DI: # Repositories _user_crud: "UserCRUD | None" _chat_config_crud: "ChatConfigCRUD | None" + _chat_config_repo: "ChatConfigRepository | None" _chat_membership_repo: "ChatMembershipRepository | None" _chat_membership_service: "ChatMembershipService | None" _chat_message_crud: "ChatMessageCRUD | None" @@ -173,6 +175,7 @@ def __init__( # Repositories self._user_crud = None self._chat_config_crud = None + self._chat_config_repo = None self._chat_membership_repo = None self._chat_membership_service = None self._chat_message_crud = None @@ -356,6 +359,13 @@ def chat_config_crud(self) -> "ChatConfigCRUD": self._chat_config_crud = ChatConfigCRUD(self.db) return self._chat_config_crud + @property + def chat_config_repo(self) -> "ChatConfigRepository": + if self._chat_config_repo is None: + from features.chat.config.chat_config_repo import ChatConfigRepository + self._chat_config_repo = ChatConfigRepository(self.db) + return self._chat_config_repo + @property def chat_membership_repo(self) -> "ChatMembershipRepository": if self._chat_membership_repo is None: diff --git a/src/features/chat/config/chat_config.py b/src/features/chat/config/chat_config.py new file mode 100644 index 00000000..68fc98a0 --- /dev/null +++ b/src/features/chat/config/chat_config.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass +from uuid import UUID + +from db.model.chat_config import ChatConfigDB + + +@dataclass(kw_only = True) +class ChatConfig: + + chat_id: UUID | None = None + external_id: str | None = None + language_iso_code: str | None = None + language_name: str | None = None + title: str | None = None + is_private: bool = True + reply_chance_percent: int = 100 + release_notifications: ChatConfigDB.ReleaseNotifications = ChatConfigDB.ReleaseNotifications.major + media_mode: ChatConfigDB.MediaMode = ChatConfigDB.MediaMode.photo + chat_type: ChatConfigDB.ChatType diff --git a/src/features/chat/config/chat_config_mapper.py b/src/features/chat/config/chat_config_mapper.py new file mode 100644 index 00000000..5e44f9a8 --- /dev/null +++ b/src/features/chat/config/chat_config_mapper.py @@ -0,0 +1,73 @@ +from dataclasses import replace + +from db.model.chat_config import ChatConfigDB +from features.chat.config.chat_config import ChatConfig +from features.chat.config.chat_config_remote_data import ChatConfigRemoteData + + +def domain(db_model: ChatConfigDB | None) -> ChatConfig | None: + if db_model is None: + return None + + return ChatConfig( + chat_id = db_model.chat_id, + external_id = db_model.external_id, + language_iso_code = db_model.language_iso_code, + language_name = db_model.language_name, + title = db_model.title, + is_private = db_model.is_private, + reply_chance_percent = db_model.reply_chance_percent, + release_notifications = db_model.release_notifications, + media_mode = db_model.media_mode, + chat_type = db_model.chat_type, + ) + + +def db(domain_model: ChatConfig | None) -> ChatConfigDB | None: + if domain_model is None: + return None + + data = { + "external_id": domain_model.external_id, + "language_iso_code": domain_model.language_iso_code, + "language_name": domain_model.language_name, + "title": domain_model.title, + "is_private": domain_model.is_private, + "reply_chance_percent": domain_model.reply_chance_percent, + "release_notifications": domain_model.release_notifications, + "media_mode": domain_model.media_mode, + "chat_type": domain_model.chat_type, + } + if domain_model.chat_id is not None: + data["chat_id"] = domain_model.chat_id + return ChatConfigDB(**data) + + +def from_remote_data(remote_data: ChatConfigRemoteData) -> ChatConfig: + is_private = remote_data.is_private if remote_data.is_private is not None else True + return ChatConfig( + external_id = remote_data.external_id, + language_iso_code = remote_data.language_iso_code, + title = remote_data.title, + is_private = is_private, + reply_chance_percent = 100, + release_notifications = ( + ChatConfigDB.ReleaseNotifications.major + if is_private + else ChatConfigDB.ReleaseNotifications.none + ), + media_mode = ChatConfigDB.MediaMode.photo, + chat_type = remote_data.chat_type, + ) + + +def apply_remote_data( + chat_config: ChatConfig, + remote_data: ChatConfigRemoteData, +) -> ChatConfig: + overrides = {} + if remote_data.title is not None: + overrides["title"] = remote_data.title + if remote_data.is_private is not None: + overrides["is_private"] = remote_data.is_private + return replace(chat_config, **overrides) diff --git a/src/features/chat/config/chat_config_remote_data.py b/src/features/chat/config/chat_config_remote_data.py new file mode 100644 index 00000000..4735986f --- /dev/null +++ b/src/features/chat/config/chat_config_remote_data.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + +from db.model.chat_config import ChatConfigDB + + +@dataclass(kw_only = True) +class ChatConfigRemoteData: + external_id: str + chat_type: ChatConfigDB.ChatType + title: str | None = None + is_private: bool | None = None + language_iso_code: str | None = None diff --git a/src/features/chat/config/chat_config_repo.py b/src/features/chat/config/chat_config_repo.py new file mode 100644 index 00000000..61748dd0 --- /dev/null +++ b/src/features/chat/config/chat_config_repo.py @@ -0,0 +1,92 @@ +from uuid import UUID + +from sqlalchemy.orm import Session + +from db.model.chat_config import ChatConfigDB +from features.chat.config.chat_config import ChatConfig +from features.chat.config.chat_config_mapper import apply_remote_data, db, domain, from_remote_data +from features.chat.config.chat_config_remote_data import ChatConfigRemoteData + + +class ChatConfigRepository: + + _db: Session + + def __init__(self, db_session: Session): + self._db = db_session + + def get(self, chat_id: UUID) -> ChatConfig | None: + db_model = self._db.query(ChatConfigDB).filter( + ChatConfigDB.chat_id == chat_id, + ).first() + return domain(db_model) + + def get_all(self, skip: int = 0, limit: int = 100) -> list[ChatConfig]: + db_models = self._db.query(ChatConfigDB).offset(skip).limit(limit).all() + return [domain(m) for m in db_models if m is not None] + + def get_by_external_identifiers( + self, + external_id: str, + chat_type: ChatConfigDB.ChatType, + ) -> ChatConfig | None: + db_model = self._db.query(ChatConfigDB).filter( + ChatConfigDB.external_id == external_id, + ChatConfigDB.chat_type == chat_type, + ).first() + return domain(db_model) + + def save(self, chat_config: ChatConfig | ChatConfigRemoteData) -> ChatConfig: + if isinstance(chat_config, ChatConfigRemoteData): + return self.__save_remote_data(chat_config) + return self.__save_chat_config(chat_config) + + def delete(self, chat_id: UUID) -> ChatConfig | None: + db_model = self._db.query(ChatConfigDB).filter( + ChatConfigDB.chat_id == chat_id, + ).first() + if db_model is None: + return None + snapshot = domain(db_model) + self._db.delete(db_model) + self._db.commit() + return snapshot + + def __save_remote_data(self, remote_data: ChatConfigRemoteData) -> ChatConfig: + existing = self.get_by_external_identifiers( + external_id = remote_data.external_id, + chat_type = remote_data.chat_type, + ) + if existing is not None: + return self.__save_chat_config(apply_remote_data(existing, remote_data)) + return self.__save_chat_config(from_remote_data(remote_data)) + + def __save_chat_config(self, chat_config: ChatConfig) -> ChatConfig: + existing: ChatConfigDB | None = None + if chat_config.chat_id is not None: + existing = self._db.query(ChatConfigDB).filter( + ChatConfigDB.chat_id == chat_config.chat_id, + ).first() + + if existing is not None: + self.__copy_to_db_model(chat_config, existing) + self._db.commit() + self._db.refresh(existing) + return domain(existing) + + db_model = db(chat_config) + self._db.add(db_model) + self._db.commit() + self._db.refresh(db_model) + return domain(db_model) + + def __copy_to_db_model(self, source: ChatConfig, target: ChatConfigDB) -> None: + target.external_id = source.external_id + target.language_iso_code = source.language_iso_code + target.language_name = source.language_name + target.title = source.title + target.is_private = source.is_private + target.reply_chance_percent = source.reply_chance_percent + target.release_notifications = source.release_notifications + target.media_mode = source.media_mode + target.chat_type = source.chat_type diff --git a/test/db/sql_util.py b/test/db/sql_util.py index e57dce74..df081b92 100644 --- a/test/db/sql_util.py +++ b/test/db/sql_util.py @@ -10,6 +10,7 @@ from db.sql import initialize_db from features.accounting.purchases.purchase_record_repo import PurchaseRecordRepository 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 @@ -47,6 +48,11 @@ def chat_config_crud(self) -> ChatConfigCRUD: self.start_session() return ChatConfigCRUD(self.__session) + def chat_config_repo(self) -> ChatConfigRepository: + if not self.__is_session_active: + self.start_session() + return ChatConfigRepository(self.__session) + def chat_membership_repo(self) -> ChatMembershipRepository: if not self.__is_session_active: self.start_session() diff --git a/test/features/chat/config/test_chat_config_mapper.py b/test/features/chat/config/test_chat_config_mapper.py new file mode 100644 index 00000000..3bf3dc3c --- /dev/null +++ b/test/features/chat/config/test_chat_config_mapper.py @@ -0,0 +1,155 @@ +import unittest +from uuid import UUID + +from db.model.chat_config import ChatConfigDB +from features.chat.config.chat_config import ChatConfig +from features.chat.config.chat_config_mapper import apply_remote_data, db, domain, from_remote_data +from features.chat.config.chat_config_remote_data import ChatConfigRemoteData + + +class ChatConfigMapperTest(unittest.TestCase): + + chat_id: UUID + db_model: ChatConfigDB + domain_model: ChatConfig + + def setUp(self): + self.chat_id = UUID("11111111-1111-1111-1111-111111111111") + self.db_model = ChatConfigDB( + chat_id = self.chat_id, + external_id = "chat1", + language_iso_code = "en", + language_name = "English", + title = "Chat One", + is_private = False, + reply_chance_percent = 75, + release_notifications = ChatConfigDB.ReleaseNotifications.minor, + media_mode = ChatConfigDB.MediaMode.file, + chat_type = ChatConfigDB.ChatType.telegram, + ) + self.domain_model = ChatConfig( + chat_id = self.chat_id, + external_id = "chat1", + language_iso_code = "en", + language_name = "English", + title = "Chat One", + is_private = False, + reply_chance_percent = 75, + release_notifications = ChatConfigDB.ReleaseNotifications.minor, + media_mode = ChatConfigDB.MediaMode.file, + chat_type = ChatConfigDB.ChatType.telegram, + ) + + 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): + result = domain(self.db_model) + + self.assertIsNotNone(result) + self.assertEqual(result.chat_id, self.db_model.chat_id) + self.assertEqual(result.external_id, self.db_model.external_id) + self.assertEqual(result.language_iso_code, self.db_model.language_iso_code) + self.assertEqual(result.language_name, self.db_model.language_name) + self.assertEqual(result.title, self.db_model.title) + self.assertEqual(result.is_private, self.db_model.is_private) + self.assertEqual(result.reply_chance_percent, self.db_model.reply_chance_percent) + self.assertEqual(result.release_notifications, self.db_model.release_notifications) + self.assertEqual(result.media_mode, self.db_model.media_mode) + self.assertEqual(result.chat_type, self.db_model.chat_type) + + def test_db_maps_all_fields(self): + result = db(self.domain_model) + + self.assertIsNotNone(result) + self.assertEqual(result.chat_id, self.domain_model.chat_id) + self.assertEqual(result.external_id, self.domain_model.external_id) + self.assertEqual(result.language_iso_code, self.domain_model.language_iso_code) + self.assertEqual(result.language_name, self.domain_model.language_name) + self.assertEqual(result.title, self.domain_model.title) + self.assertEqual(result.is_private, self.domain_model.is_private) + self.assertEqual(result.reply_chance_percent, self.domain_model.reply_chance_percent) + self.assertEqual(result.release_notifications, self.domain_model.release_notifications) + self.assertEqual(result.media_mode, self.domain_model.media_mode) + self.assertEqual(result.chat_type, self.domain_model.chat_type) + + def test_roundtrip_domain_to_db_to_domain(self): + result = domain(db(self.domain_model)) + + self.assertEqual(result, self.domain_model) + + def test_db_leaves_missing_chat_id_for_database_generation(self): + self.domain_model.chat_id = None + + result = db(self.domain_model) + + self.assertIsNone(result.chat_id) + + def test_from_remote_data_defaults_missing_privacy_to_private(self): + remote_data = ChatConfigRemoteData( + external_id = "chat1", + chat_type = ChatConfigDB.ChatType.telegram, + title = "Chat One", + language_iso_code = "en", + ) + + result = from_remote_data(remote_data) + + self.assertIsNone(result.chat_id) + self.assertEqual(result.external_id, remote_data.external_id) + self.assertEqual(result.language_iso_code, remote_data.language_iso_code) + self.assertEqual(result.title, remote_data.title) + self.assertTrue(result.is_private) + self.assertEqual(result.reply_chance_percent, 100) + self.assertEqual(result.release_notifications, ChatConfigDB.ReleaseNotifications.major) + self.assertEqual(result.media_mode, ChatConfigDB.MediaMode.photo) + self.assertEqual(result.chat_type, remote_data.chat_type) + + def test_from_remote_data_sets_public_release_defaults(self): + remote_data = ChatConfigRemoteData( + external_id = "chat1", + chat_type = ChatConfigDB.ChatType.telegram, + title = "Public Chat", + is_private = False, + ) + + result = from_remote_data(remote_data) + + self.assertFalse(result.is_private) + self.assertEqual(result.release_notifications, ChatConfigDB.ReleaseNotifications.none) + + def test_apply_remote_data_updates_only_remote_owned_fields(self): + remote_data = ChatConfigRemoteData( + external_id = "chat1", + chat_type = ChatConfigDB.ChatType.telegram, + title = "Updated Title", + is_private = True, + language_iso_code = "fr", + ) + + result = apply_remote_data(self.domain_model, remote_data) + + self.assertEqual(result.chat_id, self.domain_model.chat_id) + self.assertEqual(result.external_id, self.domain_model.external_id) + self.assertEqual(result.language_iso_code, self.domain_model.language_iso_code) + self.assertEqual(result.language_name, self.domain_model.language_name) + self.assertEqual(result.title, "Updated Title") + self.assertTrue(result.is_private) + self.assertEqual(result.reply_chance_percent, self.domain_model.reply_chance_percent) + self.assertEqual(result.release_notifications, self.domain_model.release_notifications) + self.assertEqual(result.media_mode, self.domain_model.media_mode) + self.assertEqual(result.chat_type, self.domain_model.chat_type) + + def test_apply_remote_data_ignores_null_remote_values(self): + remote_data = ChatConfigRemoteData( + external_id = "chat1", + chat_type = ChatConfigDB.ChatType.telegram, + language_iso_code = "fr", + ) + + result = apply_remote_data(self.domain_model, remote_data) + + self.assertEqual(result, self.domain_model) diff --git a/test/features/chat/config/test_chat_config_repo.py b/test/features/chat/config/test_chat_config_repo.py new file mode 100644 index 00000000..37839587 --- /dev/null +++ b/test/features/chat/config/test_chat_config_repo.py @@ -0,0 +1,223 @@ +import unittest +from uuid import uuid4 + +from db.sql_util import SQLUtil + +from db.model.chat_config import ChatConfigDB +from features.chat.config.chat_config import ChatConfig +from features.chat.config.chat_config_remote_data import ChatConfigRemoteData +from features.chat.config.chat_config_repo import ChatConfigRepository + + +class ChatConfigRepositoryTest(unittest.TestCase): + + sql: SQLUtil + repo: ChatConfigRepository + + def setUp(self): + self.sql = SQLUtil() + self.repo = self.sql.chat_config_repo() + + def tearDown(self): + self.sql.end_session() + + def _chat_config( + self, + external_id: str = "chat1", + title: str = "Chat One", + is_private: bool = True, + chat_type: ChatConfigDB.ChatType = ChatConfigDB.ChatType.telegram, + ) -> ChatConfig: + return ChatConfig( + external_id = external_id, + language_iso_code = "en", + language_name = "English", + title = title, + is_private = is_private, + reply_chance_percent = 75, + release_notifications = ChatConfigDB.ReleaseNotifications.minor, + media_mode = ChatConfigDB.MediaMode.file, + chat_type = chat_type, + ) + + def test_save_creates_chat_config(self): + chat_config = self._chat_config() + + result = self.repo.save(chat_config) + + self.assertIsNotNone(result.chat_id) + self.assertEqual(result.external_id, chat_config.external_id) + self.assertEqual(result.language_iso_code, chat_config.language_iso_code) + self.assertEqual(result.language_name, chat_config.language_name) + self.assertEqual(result.title, chat_config.title) + self.assertEqual(result.is_private, chat_config.is_private) + self.assertEqual(result.reply_chance_percent, chat_config.reply_chance_percent) + self.assertEqual(result.release_notifications, chat_config.release_notifications) + self.assertEqual(result.media_mode, chat_config.media_mode) + self.assertEqual(result.chat_type, chat_config.chat_type) + + def test_get_returns_saved_chat_config(self): + created = self.repo.save(self._chat_config()) + + result = self.repo.get(created.chat_id) + + self.assertIsNotNone(result) + self.assertEqual(result.chat_id, created.chat_id) + self.assertEqual(result.external_id, created.external_id) + + def test_get_returns_none_when_missing(self): + result = self.repo.get(uuid4()) + + self.assertIsNone(result) + + def test_get_all_returns_saved_chat_configs(self): + first = self.repo.save(self._chat_config(external_id = "chat1")) + second = self.repo.save(self._chat_config( + external_id = "chat2", + chat_type = ChatConfigDB.ChatType.background, + )) + + results = self.repo.get_all() + + self.assertEqual({result.chat_id for result in results}, {first.chat_id, second.chat_id}) + + def test_get_by_external_identifiers_returns_saved_chat_config(self): + created = self.repo.save(self._chat_config(external_id = "chat1")) + + result = self.repo.get_by_external_identifiers( + external_id = "chat1", + chat_type = ChatConfigDB.ChatType.telegram, + ) + + self.assertIsNotNone(result) + self.assertEqual(result.chat_id, created.chat_id) + + def test_get_by_external_identifiers_returns_none_when_missing(self): + result = self.repo.get_by_external_identifiers( + external_id = "missing", + chat_type = ChatConfigDB.ChatType.telegram, + ) + + self.assertIsNone(result) + + def test_save_updates_existing_chat_config(self): + created = self.repo.save(self._chat_config()) + update = ChatConfig( + chat_id = created.chat_id, + external_id = "updated-chat", + language_iso_code = "fr", + language_name = "French", + title = "Updated Chat", + is_private = False, + reply_chance_percent = 0, + release_notifications = ChatConfigDB.ReleaseNotifications.all, + media_mode = ChatConfigDB.MediaMode.all, + chat_type = ChatConfigDB.ChatType.background, + ) + + result = self.repo.save(update) + + self.assertEqual(result.chat_id, created.chat_id) + self.assertEqual(result.external_id, update.external_id) + self.assertEqual(result.language_iso_code, update.language_iso_code) + self.assertEqual(result.language_name, update.language_name) + self.assertEqual(result.title, update.title) + self.assertEqual(result.is_private, update.is_private) + self.assertEqual(result.reply_chance_percent, update.reply_chance_percent) + self.assertEqual(result.release_notifications, update.release_notifications) + self.assertEqual(result.media_mode, update.media_mode) + self.assertEqual(result.chat_type, update.chat_type) + + def test_delete_removes_chat_config(self): + created = self.repo.save(self._chat_config()) + + result = self.repo.delete(created.chat_id) + + self.assertIsNotNone(result) + self.assertEqual(result.chat_id, created.chat_id) + self.assertIsNone(self.repo.get(created.chat_id)) + + def test_delete_returns_none_when_missing(self): + result = self.repo.delete(uuid4()) + + self.assertIsNone(result) + + def test_save_remote_data_creates_private_chat_with_defaults(self): + remote_data = ChatConfigRemoteData( + external_id = "remote-chat", + chat_type = ChatConfigDB.ChatType.telegram, + title = "Remote Chat", + language_iso_code = "en", + ) + + result = self.repo.save(remote_data) + + self.assertIsNotNone(result.chat_id) + self.assertEqual(result.external_id, remote_data.external_id) + self.assertEqual(result.language_iso_code, remote_data.language_iso_code) + self.assertIsNone(result.language_name) + self.assertEqual(result.title, remote_data.title) + self.assertTrue(result.is_private) + self.assertEqual(result.reply_chance_percent, 100) + self.assertEqual(result.release_notifications, ChatConfigDB.ReleaseNotifications.major) + self.assertEqual(result.media_mode, ChatConfigDB.MediaMode.photo) + self.assertEqual(result.chat_type, remote_data.chat_type) + + def test_save_remote_data_creates_public_chat_with_release_notifications_none(self): + remote_data = ChatConfigRemoteData( + external_id = "public-chat", + chat_type = ChatConfigDB.ChatType.telegram, + title = "Public Chat", + is_private = False, + ) + + result = self.repo.save(remote_data) + + self.assertFalse(result.is_private) + self.assertEqual(result.release_notifications, ChatConfigDB.ReleaseNotifications.none) + + def test_save_remote_data_updates_existing_remote_fields_only(self): + created = self.repo.save(self._chat_config( + external_id = "remote-chat", + title = "Old Title", + is_private = True, + )) + remote_data = ChatConfigRemoteData( + external_id = "remote-chat", + chat_type = ChatConfigDB.ChatType.telegram, + title = "New Title", + is_private = False, + language_iso_code = "fr", + ) + + result = self.repo.save(remote_data) + + self.assertEqual(result.chat_id, created.chat_id) + self.assertEqual(result.external_id, created.external_id) + self.assertEqual(result.language_iso_code, created.language_iso_code) + self.assertEqual(result.language_name, created.language_name) + self.assertEqual(result.title, remote_data.title) + self.assertFalse(result.is_private) + self.assertEqual(result.reply_chance_percent, created.reply_chance_percent) + self.assertEqual(result.release_notifications, created.release_notifications) + self.assertEqual(result.media_mode, created.media_mode) + self.assertEqual(result.chat_type, created.chat_type) + + def test_save_remote_data_preserves_existing_fields_for_null_remote_values(self): + created = self.repo.save(self._chat_config( + external_id = "remote-chat", + title = "Old Title", + is_private = False, + )) + remote_data = ChatConfigRemoteData( + external_id = "remote-chat", + chat_type = ChatConfigDB.ChatType.telegram, + language_iso_code = "fr", + ) + + result = self.repo.save(remote_data) + + self.assertEqual(result.chat_id, created.chat_id) + self.assertEqual(result.language_iso_code, created.language_iso_code) + self.assertEqual(result.title, created.title) + self.assertEqual(result.is_private, created.is_private) From 1a836cfd022bd4200dd41b14c1204fcdd87ad528 Mon Sep 17 00:00:00 2001 From: Milos Marinkovic Date: Thu, 18 Jun 2026 14:25:42 +0200 Subject: [PATCH 4/7] Swap in the new Chat Config repo (and models) --- pytest.ini | 3 + src/api/authorization_service.py | 13 +- src/api/mapper/chat_settings_mapper.py | 2 +- src/api/settings_controller.py | 27 +-- src/di/di.py | 12 +- .../announcements/release_summary_service.py | 4 +- .../sys_announcements_service.py | 2 +- src/features/chat/currency_alert_service.py | 2 +- .../chat/dev_announcements_service.py | 16 +- .../membership/chat_membership_service.py | 2 +- .../chat/telegram/currency_alert_responder.py | 6 +- .../chat/telegram/domain_langchain_mapper.py | 2 +- .../telegram/release_summary_responder.py | 5 +- .../chat/telegram/telegram_data_resolver.py | 79 +------- .../chat/telegram/telegram_domain_mapper.py | 10 +- .../telegram/telegram_update_responder.py | 3 + .../chat/whatsapp/sdk/whatsapp_bot_sdk.py | 6 +- .../chat/whatsapp/whatsapp_data_resolver.py | 79 +------- .../chat/whatsapp/whatsapp_domain_mapper.py | 8 +- .../whatsapp/whatsapp_update_responder.py | 3 + src/features/integrations/integrations.py | 10 +- src/features/integrations/platform_bot_sdk.py | 2 +- src/features/integrations/prompt_resolvers.py | 16 +- test/api/mapper/test_chat_settings_mapper.py | 2 +- test/api/test_authorization_service.py | 37 ++-- test/api/test_settings_controller.py | 94 ++++------ test/api/test_sponsorships_controller.py | 3 - .../membership/test_chat_membership_repo.py | 10 +- .../test_chat_membership_service.py | 28 +-- .../telegram/test_currency_alert_responder.py | 48 ++--- .../telegram/test_domain_langchain_mapper.py | 2 +- .../test_release_summary_responder.py | 63 ++----- .../telegram/test_telegram_data_resolver.py | 173 ++---------------- .../telegram/test_telegram_domain_mapper.py | 7 +- .../test_telegram_progress_notifier.py | 2 +- .../test_telegram_update_responder.py | 17 +- test/features/chat/test_chat_agent.py | 2 +- .../chat/test_chat_attachment_processor.py | 11 -- test/features/chat/test_command_processor.py | 2 +- .../chat/test_currency_alert_service.py | 5 +- .../chat/test_dev_announcements_service.py | 20 +- .../whatsapp/sdk/test_whatsapp_bot_sdk.py | 34 +--- .../whatsapp/test_whatsapp_data_resolver.py | 169 ++--------------- .../test_whatsapp_update_responder.py | 11 +- .../integrations/test_integrations.py | 28 +-- .../integrations/test_platform_bot_sdk.py | 2 +- 46 files changed, 279 insertions(+), 803 deletions(-) diff --git a/pytest.ini b/pytest.ini index fc2b7c68..1296926d 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,5 @@ [pytest] pythonpath = src test +filterwarnings = + ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:pydub.utils + ignore:Call to deprecated method findAll.*:DeprecationWarning:readabilipy.simplifiers.html diff --git a/src/api/authorization_service.py b/src/api/authorization_service.py index 89e9bf0c..219dd2df 100644 --- a/src/api/authorization_service.py +++ b/src/api/authorization_service.py @@ -1,8 +1,8 @@ from uuid import UUID -from db.schema.chat_config import ChatConfig from db.schema.user import User from di.di import DI +from features.chat.config.chat_config import ChatConfig from features.chat.membership.chat_membership import ChatMembership from features.integrations.platform_bot_sdk import ChatAccess from util import log @@ -36,10 +36,10 @@ def validate_chat(self, chat: str | UUID | ChatConfig) -> ChatConfig: chat_uuid = chat if isinstance(chat, UUID) else UUID(hex = chat) except ValueError as e: raise ValidationError(f"Malformed chat ID '{chat}'", MALFORMED_CHAT_ID) from e - chat_config_db = self.__di.chat_config_crud.get(chat_uuid) - if not chat_config_db: + chat_config = self.__di.chat_config_repo.get(chat_uuid) + if not chat_config: raise NotFoundError(f"Chat '{chat}' not found", CHAT_NOT_FOUND) - return ChatConfig.model_validate(chat_config_db) + return chat_config def validate_user(self, user: str | UUID | User) -> User: if isinstance(user, User): @@ -60,11 +60,10 @@ def get_authorized_chats(self, user: str | UUID | User) -> list[ChatConfig]: log.t(" Validating chat configurations") max_chats = config.max_users * 10 # assuming each user administers 10 chats - all_chat_configs_db = self.__di.chat_config_crud.get_all(limit = max_chats) - if not all_chat_configs_db: + all_chat_configs = self.__di.chat_config_repo.get_all(limit = max_chats) + if not all_chat_configs: log.t(" No chat configurations found in DB") return [] - all_chat_configs = [ChatConfig.model_validate(chat_config_db) for chat_config_db in all_chat_configs_db] log.t(f" Found {len(all_chat_configs)} chat configurations to check") log.t(" Checking admin status in each chat") diff --git a/src/api/mapper/chat_settings_mapper.py b/src/api/mapper/chat_settings_mapper.py index 49267f41..eb317049 100644 --- a/src/api/mapper/chat_settings_mapper.py +++ b/src/api/mapper/chat_settings_mapper.py @@ -1,7 +1,7 @@ from api.model.chat_config_response import ChatConfigResponse from api.model.chat_settings_response import ChatSettingsResponse from api.model.user_chat_config_response import UserChatConfigResponse -from db.schema.chat_config import ChatConfig +from features.chat.config.chat_config import ChatConfig from features.chat.membership.chat_membership import ChatMembership diff --git a/src/api/settings_controller.py b/src/api/settings_controller.py index 7b3e0384..8cf72e40 100644 --- a/src/api/settings_controller.py +++ b/src/api/settings_controller.py @@ -16,9 +16,9 @@ from api.model.user_settings_payload import UserSettingsPayload from api.model.user_settings_response import UserSettingsResponse from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig, ChatConfigSave from db.schema.user import User from di.di import DI +from features.chat.config.chat_config import ChatConfig from features.chat.membership.chat_membership import ChatMembership from features.external_tools.external_tool_library import ALL_EXTERNAL_TOOLS from features.external_tools.external_tool_provider_library import ALL_PROVIDERS @@ -176,11 +176,10 @@ def fetch_all_chat_settings(self) -> list[ChatSettingsResponse]: results: list[tuple[ChatConfig, ChatMembership]] = [] for membership in memberships: - chat_config_db = self.__di.chat_config_crud.get(membership.chat_id) - if not chat_config_db: + chat_config = self.__di.chat_config_repo.get(membership.chat_id) + if not chat_config: log.t(f" Skipping orphan membership for chat '{membership.chat_id}' (chat config missing)") continue - chat_config = ChatConfig.model_validate(chat_config_db) results.append((chat_config, membership)) # sort: private/own first, then by chat type, then by title @@ -254,36 +253,38 @@ def __apply_user_chat_config_changes( ) def __apply_chat_config_changes(self, chat_config: ChatConfig, payload: ChatConfigPayload) -> None: - chat_config_save = ChatConfigSave(**chat_config.model_dump()) - # validate language changes if not payload.language_name or not payload.language_iso_code: raise ValidationError("Both language_name and language_iso_code must be non-empty", INVALID_LANGUAGE_SETTINGS) log.t(f" Updating language to '{payload.language_name}' ({payload.language_iso_code})") - chat_config_save.language_name = payload.language_name - chat_config_save.language_iso_code = payload.language_iso_code # validate reply chance changes - if chat_config_save.is_private and payload.reply_chance_percent != 100: + if chat_config.is_private and payload.reply_chance_percent != 100: raise ValidationError("Chat is private, reply chance cannot be changed", INVALID_REPLY_CHANCE) log.t(f" Updating reply chance to {payload.reply_chance_percent}%") - chat_config_save.reply_chance_percent = payload.reply_chance_percent # validate release notifications changes release_notifications = ChatConfigDB.ReleaseNotifications.lookup(payload.release_notifications) if not release_notifications: raise ValidationError(f"Invalid release notifications setting value '{payload.release_notifications}'", INVALID_RELEASE_NOTIFICATIONS) # noqa: E501 log.t(f" Updating release notifications to '{release_notifications.value}'") - chat_config_save.release_notifications = release_notifications # validate media mode changes media_mode = ChatConfigDB.MediaMode.lookup(payload.media_mode) if not media_mode: raise ValidationError(f"Invalid media mode setting value '{payload.media_mode}'", INVALID_MEDIA_MODE) log.t(f" Updating media mode to '{media_mode.value}'") - chat_config_save.media_mode = media_mode - ChatConfig.model_validate(self.__di.chat_config_crud.save(chat_config_save)) + updated_chat_config = replace( + chat_config, + language_name = payload.language_name, + language_iso_code = payload.language_iso_code, + reply_chance_percent = payload.reply_chance_percent, + release_notifications = release_notifications, + media_mode = media_mode, + ) + + self.__di.chat_config_repo.save(updated_chat_config) def save_user_settings(self, user_id_hex: str, payload: UserSettingsPayload): log.d(f"Saving user settings for user '{user_id_hex}'") diff --git a/src/di/di.py b/src/di/di.py index bcd7e4d9..2db6e87d 100644 --- a/src/di/di.py +++ b/src/di/di.py @@ -6,8 +6,8 @@ from sqlalchemy.orm import Session from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig from db.schema.user import User +from features.chat.config.chat_config import ChatConfig from util.config import config from util.error_codes import DI_DEPENDENCY_NOT_MET from util.errors import InternalError @@ -28,7 +28,6 @@ from api.sponsorships_controller import SponsorshipsController from api.transfers_controller import TransfersController from api.usage_controller import UsageController - from db.crud.chat_config import ChatConfigCRUD from db.crud.chat_message import ChatMessageCRUD from db.crud.chat_message_attachment import ChatMessageAttachmentCRUD from db.crud.price_alert import PriceAlertCRUD @@ -114,7 +113,6 @@ class DI: _whatsapp_bot_sdk: "WhatsAppBotSDK | None" # Repositories _user_crud: "UserCRUD | None" - _chat_config_crud: "ChatConfigCRUD | None" _chat_config_repo: "ChatConfigRepository | None" _chat_membership_repo: "ChatMembershipRepository | None" _chat_membership_service: "ChatMembershipService | None" @@ -174,7 +172,6 @@ def __init__( self._whatsapp_bot_sdk = None # Repositories self._user_crud = None - self._chat_config_crud = None self._chat_config_repo = None self._chat_membership_repo = None self._chat_membership_service = None @@ -352,13 +349,6 @@ def user_crud(self) -> "UserCRUD": self._user_crud = UserCRUD(self.db) return self._user_crud - @property - def chat_config_crud(self) -> "ChatConfigCRUD": - if self._chat_config_crud is None: - from db.crud.chat_config import ChatConfigCRUD - self._chat_config_crud = ChatConfigCRUD(self.db) - return self._chat_config_crud - @property def chat_config_repo(self) -> "ChatConfigRepository": if self._chat_config_repo is None: diff --git a/src/features/announcements/release_summary_service.py b/src/features/announcements/release_summary_service.py index 2291178c..02858e25 100644 --- a/src/features/announcements/release_summary_service.py +++ b/src/features/announcements/release_summary_service.py @@ -2,8 +2,8 @@ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig, ChatConfigSave from di.di import DI +from features.chat.config.chat_config import ChatConfig from features.external_tools.configured_tool import ConfiguredTool from features.external_tools.external_tool import ToolType from features.integrations import prompt_resolvers @@ -23,7 +23,7 @@ class ReleaseSummaryService: def __init__( self, raw_notes: str, - target_chat: ChatConfig | ChatConfigSave | None, + target_chat: ChatConfig | None, configured_tool: ConfiguredTool, di: DI, ): diff --git a/src/features/announcements/sys_announcements_service.py b/src/features/announcements/sys_announcements_service.py index c882c84f..dd91f065 100644 --- a/src/features/announcements/sys_announcements_service.py +++ b/src/features/announcements/sys_announcements_service.py @@ -1,8 +1,8 @@ from langchain_core.language_models import BaseChatModel from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage -from db.schema.chat_config import ChatConfig from di.di import DI +from features.chat.config.chat_config import ChatConfig from features.external_tools.configured_tool import ConfiguredTool from features.external_tools.external_tool import ToolType from features.integrations import prompt_resolvers diff --git a/src/features/chat/currency_alert_service.py b/src/features/chat/currency_alert_service.py index 2385e6b9..ccbab2e0 100644 --- a/src/features/chat/currency_alert_service.py +++ b/src/features/chat/currency_alert_service.py @@ -6,9 +6,9 @@ from db.model.chat_config import ChatConfigDB from db.model.price_alert import PriceAlertDB -from db.schema.chat_config import ChatConfig from db.schema.price_alert import PriceAlert, PriceAlertSave from di.di import DI +from features.chat.config.chat_config import ChatConfig from features.integrations.integrations import resolve_agent_user from util import log from util.error_codes import BOT_CANNOT_SET_ALERTS, NO_PRIVATE_CHAT diff --git a/src/features/chat/dev_announcements_service.py b/src/features/chat/dev_announcements_service.py index af927c6c..3c27149a 100644 --- a/src/features/chat/dev_announcements_service.py +++ b/src/features/chat/dev_announcements_service.py @@ -3,9 +3,9 @@ from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfig from db.schema.user import User from di.di import DI +from features.chat.config.chat_config import ChatConfig from features.external_tools.configured_tool import ConfiguredTool from features.external_tools.external_tool import ToolType from features.integrations import prompt_resolvers @@ -55,13 +55,9 @@ def __validate(self, target_handle: str | None): if not external_id: raise AuthorizationError(f"Target user '{target_handle}' has no external ID for {chat_type.value}", NO_PRIVATE_CHAT) # noqa: E501 - target_chat_db = self.__di.chat_config_crud.get_by_external_identifiers( - external_id = external_id, - chat_type = chat_type, - ) - if not target_chat_db: + self.__target_chat = self.__di.chat_config_repo.get_by_external_identifiers(external_id, chat_type) + if not self.__target_chat: raise NotFoundError(f"Target chat '{external_id}' not found", TARGET_CHAT_NOT_FOUND) - self.__target_chat = ChatConfig.model_validate(target_chat_db) def execute(self) -> dict: log.t(f"Executing announcement from {self.__di.invoker.id.hex}") @@ -76,10 +72,10 @@ def execute(self) -> dict: invoker_external_id = resolve_external_id(self.__di.invoker, chat_type) or "" agent_user = resolve_agent_user(chat_type) bot_external_id = resolve_external_id(agent_user, chat_type) or "" - target_chats_db = self.__di.chat_config_crud.get_all(limit = 2048) + target_chats_all = self.__di.chat_config_repo.get_all(limit = 2048) target_chats = [ - ChatConfig.model_validate(chat) - for chat in target_chats_db + chat + for chat in target_chats_all if chat.external_id not in [bot_external_id, invoker_external_id] ] diff --git a/src/features/chat/membership/chat_membership_service.py b/src/features/chat/membership/chat_membership_service.py index bd202c72..c4dcb598 100644 --- a/src/features/chat/membership/chat_membership_service.py +++ b/src/features/chat/membership/chat_membership_service.py @@ -1,8 +1,8 @@ from uuid import UUID -from db.schema.chat_config import ChatConfig from db.schema.user import User from di.di import DI +from features.chat.config.chat_config import ChatConfig from features.chat.membership.chat_membership import ChatMembership from features.integrations.platform_bot_sdk import ChatAccess from util import log diff --git a/src/features/chat/telegram/currency_alert_responder.py b/src/features/chat/telegram/currency_alert_responder.py index f6be39fd..64e756fe 100644 --- a/src/features/chat/telegram/currency_alert_responder.py +++ b/src/features/chat/telegram/currency_alert_responder.py @@ -1,6 +1,5 @@ import json -from db.schema.chat_config import ChatConfig from di.di import DI from features.announcements.sys_announcements_service import SysAnnouncementsService from features.external_tools.intelligence_presets import default_tool_for @@ -21,10 +20,9 @@ def respond_with_currency_alerts(di: DI) -> dict: scoped_di: DI # try to summarize the announcement first try: - chat_config_db = di.chat_config_crud.get(triggered_alert.chat_id) - if not chat_config_db: + chat_config = di.chat_config_repo.get(triggered_alert.chat_id) + if not chat_config: raise NotFoundError(f"Chat config not found for chat {triggered_alert.chat_id}", CHAT_CONFIG_NOT_FOUND) - chat_config = ChatConfig.model_validate(chat_config_db) scoped_di = di.clone(invoker_id = triggered_alert.owner_id.hex, invoker_chat_id = chat_config.chat_id.hex) # find the correct translations cache for this alert diff --git a/src/features/chat/telegram/domain_langchain_mapper.py b/src/features/chat/telegram/domain_langchain_mapper.py index 78a3b7e5..d82f0e91 100644 --- a/src/features/chat/telegram/domain_langchain_mapper.py +++ b/src/features/chat/telegram/domain_langchain_mapper.py @@ -6,9 +6,9 @@ from langchain_core.messages import AIMessage, HumanMessage from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig from db.schema.chat_message import ChatMessage, ChatMessageSave from db.schema.user import User +from features.chat.config.chat_config import ChatConfig from features.integrations.integrations import is_the_agent, resolve_agent_user, resolve_external_handle, resolve_external_id from features.prompting.prompt_library import CHAT_MESSAGE_DELIMITER from util import log diff --git a/src/features/chat/telegram/release_summary_responder.py b/src/features/chat/telegram/release_summary_responder.py index 2b97bff6..e4318c8c 100644 --- a/src/features/chat/telegram/release_summary_responder.py +++ b/src/features/chat/telegram/release_summary_responder.py @@ -6,9 +6,9 @@ from api.model.release_output_payload import ReleaseOutputPayload from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig from di.di import DI from features.announcements.release_summary_service import ReleaseSummaryService +from features.chat.config.chat_config import ChatConfig from features.external_tools.intelligence_presets import default_tool_for from util import log from util.config import config @@ -108,8 +108,7 @@ def respond_with_summary(payload: ReleaseOutputPayload, di: DI) -> dict: # prepare and filter the eligible chats change_type = get_version_change_type(latest_version, new_target_version) - latest_chats_db = di.chat_config_crud.get_all(limit = 2048) - latest_chats = [ChatConfig.model_validate(chat_db) for chat_db in latest_chats_db] + latest_chats = di.chat_config_repo.get_all(limit = 2048) subscribed_chats = [chat for chat in latest_chats if is_chat_subscribed(chat, change_type)] result.chats_eligible = len(latest_chats) result.chats_subscribed = len(subscribed_chats) diff --git a/src/features/chat/telegram/telegram_data_resolver.py b/src/features/chat/telegram/telegram_data_resolver.py index 4c263b6c..e35a8897 100644 --- a/src/features/chat/telegram/telegram_data_resolver.py +++ b/src/features/chat/telegram/telegram_data_resolver.py @@ -3,11 +3,11 @@ from pydantic import BaseModel from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig, ChatConfigSave from db.schema.chat_message import ChatMessage, ChatMessageSave from db.schema.chat_message_attachment import ChatMessageAttachment, ChatMessageAttachmentSave from db.schema.user import User, UserSave from di.di import DI +from features.chat.config.chat_config import ChatConfig from features.chat.telegram.telegram_domain_mapper import TelegramDomainMapper from features.integrations.integrations import is_the_agent from util import log @@ -35,7 +35,7 @@ def __init__(self, di: DI): def resolve(self, mapping_result: TelegramDomainMapper.Result) -> Result: log.t(f"Resolving mapping result: {mapping_result}") - resolved_chat_config = self.resolve_chat_config(mapping_result.chat) + resolved_chat_config = self.__di.chat_config_repo.save(mapping_result.chat) resolved_author: User | None = None is_author_the_agent = False if mapping_result.author: @@ -61,31 +61,6 @@ def resolve(self, mapping_result: TelegramDomainMapper.Result) -> Result: attachments = resolved_attachments, ) - def resolve_chat_config(self, mapped_data: ChatConfigSave) -> ChatConfig: - log.t(f" Resolving chat config: {mapped_data}") - old_chat_config_db = self.__di.chat_config_crud.get_by_external_identifiers( - external_id = str(mapped_data.external_id), - chat_type = mapped_data.chat_type, - ) - if old_chat_config_db: - old_chat_config = ChatConfig.model_validate(old_chat_config_db) - # reset the attributes that are not normally changed through the Telegram API - mapped_data.chat_id = old_chat_config.chat_id - mapped_data.language_iso_code = old_chat_config.language_iso_code - mapped_data.language_name = old_chat_config.language_name - mapped_data.is_private = old_chat_config.is_private - mapped_data.reply_chance_percent = old_chat_config.reply_chance_percent - mapped_data.release_notifications = old_chat_config.release_notifications - mapped_data.media_mode = old_chat_config.media_mode - else: - # new chat, let's set sensible default values - mapped_data.media_mode = ChatConfigDB.MediaMode.photo - if mapped_data.is_private: - mapped_data.release_notifications = ChatConfigDB.ReleaseNotifications.major - else: - mapped_data.release_notifications = ChatConfigDB.ReleaseNotifications.none - return ChatConfig.model_validate(self.__di.chat_config_crud.save(mapped_data)) - # noinspection DuplicatedCode def resolve_author(self, mapped_data: UserSave | None) -> User | None: if not mapped_data: @@ -140,56 +115,6 @@ def resolve_author(self, mapped_data: UserSave | None) -> User | None: mapped_data.is_invited_to_start = False mapped_data.are_policies_accepted = False - # reset token values to None if they are non-null but contain only whitespace - def is_empty_secret(secret): - if hasattr(secret, "get_secret_value"): - return not secret.get_secret_value().strip() - return not secret.strip() if isinstance(secret, str) else False - - # reset all SecretStr fields that are non-null but empty/whitespace - secret_fields = mapped_data._get_secret_str_fields() - for field_name in secret_fields: - field_value = getattr(mapped_data, field_name) - if field_value is not None and is_empty_secret(field_value): - log.d(f"Resetting {field_name} to None because it is empty") - setattr(mapped_data, field_name, None) - # reset tool choice values to None if they are empty strings (not if already None) - if mapped_data.tool_choice_chat is not None and not mapped_data.tool_choice_chat.strip(): - log.w("Resetting tool_choice_chat to None because it is empty") - mapped_data.tool_choice_chat = None - if mapped_data.tool_choice_reasoning is not None and not mapped_data.tool_choice_reasoning.strip(): - log.w("Resetting tool_choice_reasoning to None because it is empty") - mapped_data.tool_choice_reasoning = None - if mapped_data.tool_choice_copywriting is not None and not mapped_data.tool_choice_copywriting.strip(): - log.w("Resetting tool_choice_copywriting to None because it is empty") - mapped_data.tool_choice_copywriting = None - if mapped_data.tool_choice_vision is not None and not mapped_data.tool_choice_vision.strip(): - log.w("Resetting tool_choice_vision to None because it is empty") - mapped_data.tool_choice_vision = None - if mapped_data.tool_choice_hearing is not None and not mapped_data.tool_choice_hearing.strip(): - log.w("Resetting tool_choice_hearing to None because it is empty") - mapped_data.tool_choice_hearing = None - if mapped_data.tool_choice_images_gen is not None and not mapped_data.tool_choice_images_gen.strip(): - log.w("Resetting tool_choice_images_gen to None because it is empty") - mapped_data.tool_choice_images_gen = None - if mapped_data.tool_choice_images_edit is not None and not mapped_data.tool_choice_images_edit.strip(): - log.w("Resetting tool_choice_images_edit to None because it is empty") - mapped_data.tool_choice_images_edit = None - if mapped_data.tool_choice_search is not None and not mapped_data.tool_choice_search.strip(): - log.w("Resetting tool_choice_search to None because it is empty") - mapped_data.tool_choice_search = None - if mapped_data.tool_choice_embedding is not None and not mapped_data.tool_choice_embedding.strip(): - log.w("Resetting tool_choice_embedding to None because it is empty") - mapped_data.tool_choice_embedding = None - if mapped_data.tool_choice_api_fiat_exchange is not None and not mapped_data.tool_choice_api_fiat_exchange.strip(): - log.w("Resetting tool_choice_api_fiat_exchange to None because it is empty") - mapped_data.tool_choice_api_fiat_exchange = None - if mapped_data.tool_choice_api_crypto_exchange is not None and not mapped_data.tool_choice_api_crypto_exchange.strip(): - log.w("Resetting tool_choice_api_crypto_exchange to None because it is empty") - mapped_data.tool_choice_api_crypto_exchange = None - if mapped_data.tool_choice_api_twitter is not None and not mapped_data.tool_choice_api_twitter.strip(): - log.w("Resetting tool_choice_api_twitter to None because it is empty") - mapped_data.tool_choice_api_twitter = None return User.model_validate(self.__di.user_crud.save(mapped_data)) def resolve_chat_message(self, mapped_data: ChatMessageSave) -> ChatMessage: diff --git a/src/features/chat/telegram/telegram_domain_mapper.py b/src/features/chat/telegram/telegram_domain_mapper.py index 090ead02..698df4ed 100644 --- a/src/features/chat/telegram/telegram_domain_mapper.py +++ b/src/features/chat/telegram/telegram_domain_mapper.py @@ -4,10 +4,10 @@ from pydantic import BaseModel from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfigSave from db.schema.chat_message import ChatMessageSave from db.schema.chat_message_attachment import ChatMessageAttachmentSave from db.schema.user import UserSave +from features.chat.config.chat_config_remote_data import ChatConfigRemoteData from features.chat.telegram.model.attachment.file import File from features.chat.telegram.model.message import Message from features.chat.telegram.model.update import Update @@ -19,13 +19,13 @@ class TelegramDomainMapper: class Result(BaseModel): - chat: ChatConfigSave + chat: ChatConfigRemoteData author: UserSave | None message: ChatMessageSave attachments: List[ChatMessageAttachmentSave] def map_update(self, update: Update) -> Result | None: - log.t(f"Mapping Telegramupdate: {update}") + log.t(f"Mapping Telegram update: {update}") message = update.edited_message or update.message if not message: log.w(f" Nothing to map in update: {update}") @@ -93,12 +93,12 @@ def map_text(self, message: Message) -> str: log.t(f" Mapping message text: {parts}") return "\n\n".join(parts) - def map_chat(self, message: Message) -> ChatConfigSave: + def map_chat(self, message: Message) -> ChatConfigRemoteData: chat = message.chat log.t(f" Mapping chat: {chat}") title = self.resolve_chat_name(str(chat.id), chat.title, chat.username, chat.first_name, chat.last_name) language_code = message.from_user.language_code if message.from_user else None - return ChatConfigSave( + return ChatConfigRemoteData( external_id = str(chat.id), title = title, is_private = chat.type == "private", diff --git a/src/features/chat/telegram/telegram_update_responder.py b/src/features/chat/telegram/telegram_update_responder.py index ee5faf64..a1c93fb6 100644 --- a/src/features/chat/telegram/telegram_update_responder.py +++ b/src/features/chat/telegram/telegram_update_responder.py @@ -1,4 +1,5 @@ from datetime import datetime +from time import sleep from fastapi import HTTPException from langchain_core.messages import AIMessage @@ -76,6 +77,7 @@ def respond_to_update(update: Update) -> bool: domain_messages = di.domain_langchain_mapper.map_bot_message_to_storage(resolved_domain_data.chat, answer) for message in domain_messages: di.telegram_bot_sdk.send_text_message(str(resolved_domain_data.chat.external_id), message.text) + sleep(0.1) sent_messages += 1 log.t(f"Finished responding to updates. \n[{agent.full_name}]: {answer.content}") @@ -99,4 +101,5 @@ def __notify_of_errors( messages = di.domain_langchain_mapper.map_bot_message_to_storage(resolved_domain_data.chat, answer) for message in messages: di.telegram_bot_sdk.send_text_message(str(resolved_domain_data.chat.external_id), message.text) + sleep(0.1) log.t("Replied with the error") diff --git a/src/features/chat/whatsapp/sdk/whatsapp_bot_sdk.py b/src/features/chat/whatsapp/sdk/whatsapp_bot_sdk.py index ac37ced9..7fd14473 100644 --- a/src/features/chat/whatsapp/sdk/whatsapp_bot_sdk.py +++ b/src/features/chat/whatsapp/sdk/whatsapp_bot_sdk.py @@ -4,7 +4,6 @@ import requests from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig from db.schema.chat_message import ChatMessage, ChatMessageSave from db.schema.chat_message_attachment import ChatMessageAttachment, ChatMessageAttachmentSave from di.di import DI @@ -134,13 +133,12 @@ def __store_api_response_as_message( ) -> ChatMessage: log.t("Storing API message data...") first_message = raw_api_response.messages[0] - chat_config_db = self.__di.chat_config_crud.get_by_external_identifiers( + chat_config = self.__di.chat_config_repo.get_by_external_identifiers( external_id = recipient_id, chat_type = ChatConfigDB.ChatType.whatsapp, ) - if not chat_config_db: + if not chat_config: raise NotFoundError(f"Chat config not found for WhatsApp recipient: {recipient_id}", CHAT_CONFIG_NOT_FOUND) - chat_config = ChatConfig.model_validate(chat_config_db) message_save = ChatMessageSave( message_id = first_message.id, chat_id = chat_config.chat_id, diff --git a/src/features/chat/whatsapp/whatsapp_data_resolver.py b/src/features/chat/whatsapp/whatsapp_data_resolver.py index abd653bc..2ea52d27 100644 --- a/src/features/chat/whatsapp/whatsapp_data_resolver.py +++ b/src/features/chat/whatsapp/whatsapp_data_resolver.py @@ -3,11 +3,11 @@ from pydantic import BaseModel from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig, ChatConfigSave from db.schema.chat_message import ChatMessage, ChatMessageSave from db.schema.chat_message_attachment import ChatMessageAttachment, ChatMessageAttachmentSave from db.schema.user import User, UserSave from di.di import DI +from features.chat.config.chat_config import ChatConfig from features.chat.whatsapp.whatsapp_domain_mapper import WhatsAppDomainMapper from features.integrations.integrations import is_the_agent from util import log @@ -41,7 +41,7 @@ def resolve_all(self, mapping_results: list[WhatsAppDomainMapper.Result]) -> lis def resolve(self, mapping_result: WhatsAppDomainMapper.Result) -> Result: log.t(f"Resolving mapping result: {mapping_result}") - resolved_chat_config = self.resolve_chat_config(mapping_result.chat) + resolved_chat_config = self.__di.chat_config_repo.save(mapping_result.chat) resolved_author: User | None = None is_author_the_agent = False if mapping_result.author: @@ -77,31 +77,6 @@ def resolve(self, mapping_result: WhatsAppDomainMapper.Result) -> Result: attachments = resolved_attachments, ) - def resolve_chat_config(self, mapped_data: ChatConfigSave) -> ChatConfig: - log.t(f" Resolving chat config: {mapped_data}") - old_chat_config_db = self.__di.chat_config_crud.get_by_external_identifiers( - external_id = str(mapped_data.external_id), - chat_type = mapped_data.chat_type, - ) - if old_chat_config_db: - old_chat_config = ChatConfig.model_validate(old_chat_config_db) - # reset the attributes that are not normally changed through the WhatsApp API - mapped_data.chat_id = old_chat_config.chat_id - mapped_data.language_iso_code = old_chat_config.language_iso_code - mapped_data.language_name = old_chat_config.language_name - mapped_data.is_private = old_chat_config.is_private - mapped_data.reply_chance_percent = old_chat_config.reply_chance_percent - mapped_data.release_notifications = old_chat_config.release_notifications - mapped_data.media_mode = old_chat_config.media_mode - else: - # new chat, let's set sensible default values - mapped_data.media_mode = ChatConfigDB.MediaMode.photo - if mapped_data.is_private: - mapped_data.release_notifications = ChatConfigDB.ReleaseNotifications.major - else: - mapped_data.release_notifications = ChatConfigDB.ReleaseNotifications.none - return ChatConfig.model_validate(self.__di.chat_config_crud.save(mapped_data)) - # noinspection DuplicatedCode def resolve_author(self, mapped_data: UserSave | None) -> User | None: if not mapped_data: @@ -158,56 +133,6 @@ def resolve_author(self, mapped_data: UserSave | None) -> User | None: mapped_data.is_invited_to_start = False mapped_data.are_policies_accepted = False - # reset token values to None if they are non-null but contain only whitespace - def is_empty_secret(secret): - if hasattr(secret, "get_secret_value"): - return not secret.get_secret_value().strip() - return not secret.strip() if isinstance(secret, str) else False - - # reset all SecretStr fields that are non-null but empty/whitespace - secret_fields = mapped_data._get_secret_str_fields() - for field_name in secret_fields: - field_value = getattr(mapped_data, field_name) - if field_value is not None and is_empty_secret(field_value): - log.d(f"Resetting {field_name} to None because it is empty") - setattr(mapped_data, field_name, None) - # reset tool choice values to None if they are empty strings (not if already None) - if mapped_data.tool_choice_chat is not None and not mapped_data.tool_choice_chat.strip(): - log.w("Resetting tool_choice_chat to None because it is empty") - mapped_data.tool_choice_chat = None - if mapped_data.tool_choice_reasoning is not None and not mapped_data.tool_choice_reasoning.strip(): - log.w("Resetting tool_choice_reasoning to None because it is empty") - mapped_data.tool_choice_reasoning = None - if mapped_data.tool_choice_copywriting is not None and not mapped_data.tool_choice_copywriting.strip(): - log.w("Resetting tool_choice_copywriting to None because it is empty") - mapped_data.tool_choice_copywriting = None - if mapped_data.tool_choice_vision is not None and not mapped_data.tool_choice_vision.strip(): - log.w("Resetting tool_choice_vision to None because it is empty") - mapped_data.tool_choice_vision = None - if mapped_data.tool_choice_hearing is not None and not mapped_data.tool_choice_hearing.strip(): - log.w("Resetting tool_choice_hearing to None because it is empty") - mapped_data.tool_choice_hearing = None - if mapped_data.tool_choice_images_gen is not None and not mapped_data.tool_choice_images_gen.strip(): - log.w("Resetting tool_choice_images_gen to None because it is empty") - mapped_data.tool_choice_images_gen = None - if mapped_data.tool_choice_images_edit is not None and not mapped_data.tool_choice_images_edit.strip(): - log.w("Resetting tool_choice_images_edit to None because it is empty") - mapped_data.tool_choice_images_edit = None - if mapped_data.tool_choice_search is not None and not mapped_data.tool_choice_search.strip(): - log.w("Resetting tool_choice_search to None because it is empty") - mapped_data.tool_choice_search = None - if mapped_data.tool_choice_embedding is not None and not mapped_data.tool_choice_embedding.strip(): - log.w("Resetting tool_choice_embedding to None because it is empty") - mapped_data.tool_choice_embedding = None - if mapped_data.tool_choice_api_fiat_exchange is not None and not mapped_data.tool_choice_api_fiat_exchange.strip(): - log.w("Resetting tool_choice_api_fiat_exchange to None because it is empty") - mapped_data.tool_choice_api_fiat_exchange = None - if mapped_data.tool_choice_api_crypto_exchange is not None and not mapped_data.tool_choice_api_crypto_exchange.strip(): - log.w("Resetting tool_choice_api_crypto_exchange to None because it is empty") - mapped_data.tool_choice_api_crypto_exchange = None - if mapped_data.tool_choice_api_twitter is not None and not mapped_data.tool_choice_api_twitter.strip(): - log.w("Resetting tool_choice_api_twitter to None because it is empty") - mapped_data.tool_choice_api_twitter = None return User.model_validate(self.__di.user_crud.save(mapped_data)) def resolve_chat_message(self, mapped_data: ChatMessageSave) -> ChatMessage: diff --git a/src/features/chat/whatsapp/whatsapp_domain_mapper.py b/src/features/chat/whatsapp/whatsapp_domain_mapper.py index ca708702..7392bcf2 100644 --- a/src/features/chat/whatsapp/whatsapp_domain_mapper.py +++ b/src/features/chat/whatsapp/whatsapp_domain_mapper.py @@ -4,10 +4,10 @@ from pydantic import BaseModel, SecretStr from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfigSave from db.schema.chat_message import ChatMessageSave from db.schema.chat_message_attachment import ChatMessageAttachmentSave from db.schema.user import UserSave +from features.chat.config.chat_config_remote_data import ChatConfigRemoteData from features.chat.whatsapp.model.message import Message from features.chat.whatsapp.model.update import Update from features.chat.whatsapp.model.value import Value @@ -18,7 +18,7 @@ class WhatsAppDomainMapper: class Result(BaseModel): - chat: ChatConfigSave + chat: ChatConfigRemoteData author: UserSave | None message: ChatMessageSave attachments: List[ChatMessageAttachmentSave] @@ -108,14 +108,14 @@ def map_text(self, message: Message) -> str: log.t(f" Mapping message text: {parts}") return "\n\n".join(parts) - def map_chat(self, message: Message, value: Value) -> ChatConfigSave: + def map_chat(self, message: Message, value: Value) -> ChatConfigRemoteData: log.t(f" Mapping chat for message: {message}") external_id = message.from_ contacts = value.contacts or [] first_contact = contacts[0] if contacts else None profile_name = first_contact.profile.name if first_contact and first_contact.profile else None title = self.resolve_chat_name(external_id, profile_name) - return ChatConfigSave( + return ChatConfigRemoteData( external_id = external_id, title = title, is_private = True, # WhatsApp only supports private chats diff --git a/src/features/chat/whatsapp/whatsapp_update_responder.py b/src/features/chat/whatsapp/whatsapp_update_responder.py index c9e3f81d..11e96d53 100644 --- a/src/features/chat/whatsapp/whatsapp_update_responder.py +++ b/src/features/chat/whatsapp/whatsapp_update_responder.py @@ -1,4 +1,5 @@ from datetime import datetime +from time import sleep from langchain_core.messages import AIMessage @@ -81,6 +82,7 @@ def respond_to_update(update: Update) -> bool: domain_messages = di.domain_langchain_mapper.map_bot_message_to_storage(resolved_domain_data.chat, answer) for message in domain_messages: di.whatsapp_bot_sdk.send_text_message(str(resolved_domain_data.chat.external_id), message.text) + sleep(0.1) sent_messages += 1 # mark the incoming message as read @@ -107,4 +109,5 @@ def __notify_of_errors( messages = di.domain_langchain_mapper.map_bot_message_to_storage(resolved_domain_data.chat, answer) for message in messages: di.whatsapp_bot_sdk.send_text_message(str(resolved_domain_data.chat.external_id), message.text) + sleep(0.1) log.t("Replied with the error") diff --git a/src/features/integrations/integrations.py b/src/features/integrations/integrations.py index c7936f4d..d3514a00 100644 --- a/src/features/integrations/integrations.py +++ b/src/features/integrations/integrations.py @@ -5,11 +5,11 @@ from db.crud.user import UserCRUD from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfig from db.schema.chat_message import ChatMessage from db.schema.user import User, UserSave from di.di import DI from features.accounting.usage.participant_details import ParticipantInfo +from features.chat.config.chat_config import ChatConfig from features.integrations.integration_config import ( BACKGROUND_AGENT, TELEGRAM_REACTION_INITIAL_DELAY_S, @@ -257,13 +257,9 @@ def _find_private_chat(user: User, chat_type: ChatConfigDB.ChatType, di: DI) -> external_id = resolve_external_id(user, chat_type) if not external_id: return None - chat_db = di.chat_config_crud.get_by_external_identifiers( - external_id = external_id, - chat_type = chat_type, - ) - if not chat_db: + chat = di.chat_config_repo.get_by_external_identifiers(external_id, chat_type) + if not chat: return None - chat = ChatConfig.model_validate(chat_db) return chat if chat.is_private else None diff --git a/src/features/integrations/platform_bot_sdk.py b/src/features/integrations/platform_bot_sdk.py index 43e7dce0..947a3702 100644 --- a/src/features/integrations/platform_bot_sdk.py +++ b/src/features/integrations/platform_bot_sdk.py @@ -6,11 +6,11 @@ import requests from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig from db.schema.chat_message import ChatMessage from db.schema.chat_message_attachment import ChatMessageAttachment from db.schema.user import User from di.di import DI +from features.chat.config.chat_config import ChatConfig from features.images.image_size_utils import resize_file from features.integrations.integration_config import TELEGRAM_MAX_PHOTO_SIZE_BYTES, WHATSAPP_MAX_PHOTO_SIZE_BYTES from features.integrations.integrations import is_own_chat diff --git a/src/features/integrations/prompt_resolvers.py b/src/features/integrations/prompt_resolvers.py index befbb28c..1699f87f 100644 --- a/src/features/integrations/prompt_resolvers.py +++ b/src/features/integrations/prompt_resolvers.py @@ -1,8 +1,8 @@ from datetime import datetime from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig, ChatConfigSave from db.schema.user import User, UserSave +from features.chat.config.chat_config import ChatConfig from features.chat.membership.chat_membership import ChatMembership from features.integrations.integrations import resolve_agent_user, resolve_allowed_reactions from features.prompting import prompt_composer, prompt_library @@ -17,7 +17,7 @@ def chat( invoker: User | UserSave, - target_chat: ChatConfig | ChatConfigSave, + target_chat: ChatConfig, invoker_membership: ChatMembership | None, tools_list: str | None, ) -> str: @@ -94,7 +94,7 @@ def chat( def copywriting_new_release_version( chat_type: ChatConfigDB.ChatType, - target_chat: ChatConfig | ChatConfigSave | None, + target_chat: ChatConfig | None, ) -> str: # add generic components to prepare the composer agent_user = resolve_agent_user(chat_type) @@ -155,7 +155,7 @@ def copywriting_new_release_version( raise ConfigurationError(f"Unsupported chat type: {chat_type}", UNSUPPORTED_CHAT_TYPE) -def copywriting_new_system_event(target_chat: ChatConfig | ChatConfigSave) -> str: +def copywriting_new_system_event(target_chat: ChatConfig) -> str: # add generic components to prepare the composer agent_user = resolve_agent_user(target_chat.chat_type) composer = prompt_composer.build( @@ -194,7 +194,7 @@ def copywriting_new_system_event(target_chat: ChatConfig | ChatConfigSave) -> st def copywriting_system_announcement( chat_type: ChatConfigDB.ChatType, - target_chat: ChatConfig | ChatConfigSave | None, + target_chat: ChatConfig | None, ) -> str: # prepare the correct message variant (broadcast vs personal) context_copywriting_variant: PromptFragment @@ -244,7 +244,7 @@ def copywriting_system_announcement( raise ConfigurationError(f"Unsupported chat type: {chat_type}", UNSUPPORTED_CHAT_TYPE) -def sentient_web_search(target_chat: ChatConfig | ChatConfigSave) -> str: +def sentient_web_search(target_chat: ChatConfig) -> str: agent_user = resolve_agent_user(target_chat.chat_type) composer = prompt_composer.build( prompt_library.contexts.core, @@ -307,7 +307,7 @@ def computer_vision(chat_type: ChatConfigDB.ChatType) -> str: ).render() -def copywriting_computer_hearing(target_chat: ChatConfig | ChatConfigSave) -> str: +def copywriting_computer_hearing(target_chat: ChatConfig) -> str: # add generic components to prepare the composer agent_user = resolve_agent_user(target_chat.chat_type) composer = prompt_composer.build( @@ -345,7 +345,7 @@ def copywriting_computer_hearing(target_chat: ChatConfig | ChatConfigSave) -> st def document_search_and_response( query: str | None, - target_chat: ChatConfig | ChatConfigSave, + target_chat: ChatConfig, ) -> str: agent_user = resolve_agent_user(target_chat.chat_type) return prompt_composer.build( diff --git a/test/api/mapper/test_chat_settings_mapper.py b/test/api/mapper/test_chat_settings_mapper.py index 7b123a87..bd770925 100644 --- a/test/api/mapper/test_chat_settings_mapper.py +++ b/test/api/mapper/test_chat_settings_mapper.py @@ -3,7 +3,7 @@ from api.mapper.chat_settings_mapper import domain_to_api from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig +from features.chat.config.chat_config import ChatConfig from features.chat.membership.chat_membership import ChatMembership diff --git a/test/api/test_authorization_service.py b/test/api/test_authorization_service.py index ab9ba382..36b8ee65 100644 --- a/test/api/test_authorization_service.py +++ b/test/api/test_authorization_service.py @@ -6,13 +6,13 @@ from pydantic import SecretStr from api.authorization_service import AuthorizationService -from db.crud.chat_config import ChatConfigCRUD from db.crud.user import UserCRUD from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfig from db.schema.user import User from di.di import DI +from features.chat.config.chat_config import ChatConfig +from features.chat.config.chat_config_repo import ChatConfigRepository from features.chat.membership.chat_membership import ChatMembership from features.integrations.platform_bot_sdk import ChatAccess from util.error_codes import NOT_CHAT_ADMIN, NOT_CHAT_MEMBER, WAITLIST_ACCOUNT_NOT_ACTIVE, WAITLIST_INVITED_POLICIES_REQUIRED @@ -24,7 +24,7 @@ class AuthorizationServiceTest(unittest.TestCase): invoker_user: User chat_config: ChatConfig mock_user_dao: UserCRUD - mock_chat_config_dao: ChatConfigCRUD + mock_chat_config_repo: ChatConfigRepository mock_di: DI def setUp(self): @@ -52,13 +52,13 @@ def setUp(self): ) self.mock_user_dao = Mock(spec = UserCRUD) self.mock_user_dao.get.return_value = self.invoker_user - self.mock_chat_config_dao = Mock(spec = ChatConfigCRUD) - self.mock_chat_config_dao.get.return_value = self.chat_config + self.mock_chat_config_repo = Mock(spec = ChatConfigRepository) + self.mock_chat_config_repo.get.return_value = self.chat_config self.mock_di = Mock(spec = DI) # noinspection PyPropertyAccess self.mock_di.user_crud = self.mock_user_dao # noinspection PyPropertyAccess - self.mock_di.chat_config_crud = self.mock_chat_config_dao + self.mock_di.chat_config_repo = self.mock_chat_config_repo def test_validate_chat_success_with_string(self): service = AuthorizationService(self.mock_di) @@ -163,10 +163,10 @@ def test_get_authorized_chats_success_user_is_admin(self): media_mode = ChatConfigDB.MediaMode.photo, chat_type = ChatConfigDB.ChatType.telegram, ) - self.mock_chat_config_dao.get_all.return_value = [ - ChatConfigDB(**chat_config_1.model_dump()), - ChatConfigDB(**chat_config_2.model_dump()), - ChatConfigDB(**chat_config_3.model_dump()), + self.mock_chat_config_repo.get_all.return_value = [ + chat_config_1, + chat_config_2, + chat_config_3, ] admin_ids = {chat_config_1.chat_id, chat_config_3.chat_id} self.mock_di.platform_bot_sdk.return_value.resolve_chat_access.side_effect = ( @@ -204,7 +204,7 @@ def test_get_authorized_chats_success_user_administers_multiple_chats(self): media_mode = ChatConfigDB.MediaMode.photo, chat_type = ChatConfigDB.ChatType.telegram, ) - self.mock_chat_config_dao.get_all.return_value = [chat_config1, chat_config2, chat_config3] + self.mock_chat_config_repo.get_all.return_value = [chat_config1, chat_config2, chat_config3] admin_ids = {chat_config1.chat_id, chat_config3.chat_id} self.mock_di.platform_bot_sdk.return_value.resolve_chat_access.side_effect = ( lambda chat, user: ChatAccess.admin if chat.chat_id in admin_ids else None @@ -231,8 +231,7 @@ def test_get_authorized_chats_user_no_telegram_id(self): created_at = datetime.now().date(), ) - # Mock chat_config_crud.get_all to return empty list - self.mock_chat_config_dao.get_all.return_value = [] + self.mock_chat_config_repo.get_all.return_value = [] service = AuthorizationService(self.mock_di) admin_chats = service.get_authorized_chats(user_without_telegram_id) @@ -281,11 +280,12 @@ def test_get_authorized_chats_sorting_order(self): chat_type = ChatConfigDB.ChatType.telegram, ) - all_chats_db = [ - ChatConfigDB(**chat.model_dump()) for chat in - [group_chat_z, private_chat, group_chat_no_title, group_chat_a] + self.mock_chat_config_repo.get_all.return_value = [ + group_chat_z, + private_chat, + group_chat_no_title, + group_chat_a, ] - self.mock_chat_config_dao.get_all.return_value = all_chats_db self.mock_di.platform_bot_sdk.return_value.resolve_chat_access.return_value = ChatAccess.admin @@ -475,8 +475,7 @@ def test_update_chat_authorization_propagates_authorization_error(self): # === update_all_chat_authorizations === def test_update_all_chat_authorizations_delegates_to_membership_service(self): - chat_config_db = ChatConfigDB(**self.chat_config.model_dump()) - self.mock_chat_config_dao.get_all.return_value = [chat_config_db] + self.mock_chat_config_repo.get_all.return_value = [self.chat_config] self.mock_di.platform_bot_sdk.return_value.resolve_chat_access.return_value = ChatAccess.admin updated_membership = ChatMembership( user_id = self.invoker_user.id, diff --git a/test/api/test_settings_controller.py b/test/api/test_settings_controller.py index 67473e72..f97b4245 100644 --- a/test/api/test_settings_controller.py +++ b/test/api/test_settings_controller.py @@ -14,14 +14,14 @@ 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.chat_config import ChatConfigCRUD 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.chat_config import ChatConfig from db.schema.user import User from di.di import DI +from features.chat.config.chat_config import ChatConfig as ChatConfigDomain +from features.chat.config.chat_config_repo import ChatConfigRepository from features.chat.membership.chat_membership import ChatMembership from features.chat.telegram.model.chat_member import ChatMemberAdministrator from features.chat.telegram.model.user import User as TelegramUser @@ -43,10 +43,11 @@ class SettingsControllerTest(unittest.TestCase): invoker_user: User invoker_telegram_user: TelegramUser - chat_config: ChatConfig + chat_config: ChatConfigDomain + chat_config_domain: ChatConfigDomain mock_di: DI mock_user_dao: UserCRUD - mock_chat_config_dao: ChatConfigCRUD + mock_chat_config_repo: ChatConfigRepository mock_sponsorship_dao: SponsorshipCRUD mock_telegram_sdk: TelegramBotSDK mock_authorization_service: AuthorizationService @@ -81,7 +82,7 @@ def setUp(self): group = UserDB.Group.developer, created_at = datetime.now().date(), ) - self.chat_config = ChatConfig( + self.chat_config = ChatConfigDomain( chat_id = UUID(int = 1), external_id = "test_chat_123", title = "Test Chat", @@ -92,16 +93,18 @@ def setUp(self): media_mode = ChatConfigDB.MediaMode.photo, chat_type = ChatConfigDB.ChatType.telegram, ) + self.chat_config_domain = self.chat_config # Create mocks self.mock_user_dao = MagicMock(spec = UserCRUD) - self.mock_chat_config_dao = MagicMock(spec = ChatConfigCRUD) + self.mock_chat_config_repo = MagicMock(spec = ChatConfigRepository) self.mock_sponsorship_dao = MagicMock(spec = SponsorshipCRUD) 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_dao.get.return_value = self.chat_config + 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 = [] # Create mock DI container @@ -115,7 +118,7 @@ def setUp(self): # noinspection PyPropertyAccess self.mock_di.user_crud = self.mock_user_dao # noinspection PyPropertyAccess - self.mock_di.chat_config_crud = self.mock_chat_config_dao + self.mock_di.chat_config_repo = self.mock_chat_config_repo # noinspection PyPropertyAccess self.mock_di.sponsorship_crud = self.mock_sponsorship_dao # noinspection PyPropertyAccess @@ -426,7 +429,7 @@ def test_save_chat_settings_failure_language_mismatch(self): self.assertIn("Both language_name and language_iso_code must be non-empty", str(context.exception)) def test_save_chat_settings_failure_reply_chance_private_chat(self): - private_chat_config = ChatConfig( + private_chat_config = ChatConfigDomain( chat_id = UUID(int = 123), external_id = "private_chat_123", title = "Private Chat", @@ -473,19 +476,6 @@ def test_save_chat_settings_failure_invalid_release_notifications(self): self.assertIn("Invalid release notifications setting value", str(context.exception)) def test_save_chat_settings_success_chat_config(self): - saved_chat_config_db = ChatConfigDB( - chat_id = self.chat_config.chat_id, - title = self.chat_config.title, - language_iso_code = "es", - language_name = "Spanish", - reply_chance_percent = 75, - is_private = self.chat_config.is_private, - release_notifications = ChatConfigDB.ReleaseNotifications.major, - media_mode = ChatConfigDB.MediaMode.file, - chat_type = ChatConfigDB.ChatType.telegram, - ) - self.mock_chat_config_dao.save.return_value = saved_chat_config_db - controller = SettingsController(self.mock_di) payload = ChatSettingsPayload( chat_config = ChatConfigPayload( @@ -500,7 +490,19 @@ def test_save_chat_settings_success_chat_config(self): controller.save_chat_settings(self.chat_config.chat_id.hex, payload) # noinspection PyUnresolvedReferences - self.mock_chat_config_dao.save.assert_called_once() + self.mock_chat_config_repo.save.assert_called_once() + saved_chat_config = self.mock_chat_config_repo.save.call_args.args[0] + self.assertIsInstance(saved_chat_config, ChatConfigDomain) + self.assertEqual(saved_chat_config.chat_id, self.chat_config.chat_id) + self.assertEqual(saved_chat_config.external_id, self.chat_config.external_id) + self.assertEqual(saved_chat_config.title, self.chat_config.title) + self.assertEqual(saved_chat_config.language_name, "Spanish") + self.assertEqual(saved_chat_config.language_iso_code, "es") + self.assertEqual(saved_chat_config.reply_chance_percent, 75) + self.assertEqual(saved_chat_config.is_private, self.chat_config.is_private) + self.assertEqual(saved_chat_config.release_notifications, ChatConfigDB.ReleaseNotifications.major) + self.assertEqual(saved_chat_config.media_mode, ChatConfigDB.MediaMode.file) + self.assertEqual(saved_chat_config.chat_type, self.chat_config.chat_type) def test_save_chat_settings_success_user_chat_config(self): controller = SettingsController(self.mock_di) @@ -554,7 +556,7 @@ def test_save_chat_settings_rejects_empty_payload(self): controller.save_chat_settings(self.chat_config.chat_id.hex, payload) def test_fetch_all_chat_settings_success(self): - own_chat_config = ChatConfig( + own_chat_config = ChatConfigDomain( chat_id = UUID(int = 2), external_id = str(self.invoker_user.telegram_chat_id), title = "My Notes", @@ -572,19 +574,8 @@ def test_fetch_all_chat_settings_success(self): use_about_me = True, use_custom_prompt = True, ) - own_chat_db = ChatConfigDB( - chat_id = own_chat_config.chat_id, - external_id = own_chat_config.external_id, - title = own_chat_config.title, - language_iso_code = own_chat_config.language_iso_code, - reply_chance_percent = own_chat_config.reply_chance_percent, - is_private = own_chat_config.is_private, - release_notifications = own_chat_config.release_notifications, - media_mode = own_chat_config.media_mode, - chat_type = own_chat_config.chat_type, - ) self.mock_authorization_service.update_all_chat_authorizations.return_value = [own_membership] - self.mock_chat_config_dao.get.return_value = own_chat_db + self.mock_chat_config_repo.get.return_value = own_chat_config controller = SettingsController(self.mock_di) result = controller.fetch_all_chat_settings() @@ -597,7 +588,7 @@ def test_fetch_all_chat_settings_success(self): self.assertIsNotNone(result[0].user_chat_config) def test_fetch_all_chat_settings_sort_order(self): - private_config = ChatConfig( + private_config = ChatConfigDomain( chat_id = UUID(int = 1), title = "Private", language_iso_code = "en", @@ -607,7 +598,7 @@ def test_fetch_all_chat_settings_sort_order(self): media_mode = ChatConfigDB.MediaMode.photo, chat_type = ChatConfigDB.ChatType.telegram, ) - admin_config = ChatConfig( + admin_config = ChatConfigDomain( chat_id = UUID(int = 2), title = "Admin Group", language_iso_code = "en", @@ -617,7 +608,7 @@ def test_fetch_all_chat_settings_sort_order(self): media_mode = ChatConfigDB.MediaMode.photo, chat_type = ChatConfigDB.ChatType.telegram, ) - member_config = ChatConfig( + member_config = ChatConfigDomain( chat_id = UUID(int = 3), title = "Member Group", language_iso_code = "en", @@ -651,27 +642,12 @@ def test_fetch_all_chat_settings_sort_order(self): self.mock_authorization_service.update_all_chat_authorizations.return_value = [ member_membership, admin_membership, private_membership, ] - config_db_map = { - private_config.chat_id: ChatConfigDB( - chat_id = private_config.chat_id, title = private_config.title, - language_iso_code = "en", reply_chance_percent = 100, is_private = True, - release_notifications = ChatConfigDB.ReleaseNotifications.all, - media_mode = ChatConfigDB.MediaMode.photo, chat_type = ChatConfigDB.ChatType.telegram, - ), - admin_config.chat_id: ChatConfigDB( - chat_id = admin_config.chat_id, title = admin_config.title, - language_iso_code = "en", reply_chance_percent = 50, is_private = False, - release_notifications = ChatConfigDB.ReleaseNotifications.all, - media_mode = ChatConfigDB.MediaMode.photo, chat_type = ChatConfigDB.ChatType.telegram, - ), - member_config.chat_id: ChatConfigDB( - chat_id = member_config.chat_id, title = member_config.title, - language_iso_code = "en", reply_chance_percent = 50, is_private = False, - release_notifications = ChatConfigDB.ReleaseNotifications.all, - media_mode = ChatConfigDB.MediaMode.photo, chat_type = ChatConfigDB.ChatType.telegram, - ), + config_map = { + private_config.chat_id: private_config, + admin_config.chat_id: admin_config, + member_config.chat_id: member_config, } - self.mock_chat_config_dao.get.side_effect = lambda chat_id: config_db_map[chat_id] + self.mock_chat_config_repo.get.side_effect = lambda chat_id: config_map[chat_id] controller = SettingsController(self.mock_di) result = controller.fetch_all_chat_settings() diff --git a/test/api/test_sponsorships_controller.py b/test/api/test_sponsorships_controller.py index 4bc35f7c..34be1610 100644 --- a/test/api/test_sponsorships_controller.py +++ b/test/api/test_sponsorships_controller.py @@ -7,7 +7,6 @@ from api.model.sponsorship_payload import SponsorshipPayload from api.sponsorships_controller import SponsorshipsController -from db.crud.chat_config import ChatConfigCRUD from db.crud.sponsorship import SponsorshipCRUD from db.crud.user import UserCRUD from db.model.chat_config import ChatConfigDB @@ -82,8 +81,6 @@ def setUp(self): # noinspection PyPropertyAccess self.mock_di.sponsorship_crud = Mock(spec = SponsorshipCRUD) # noinspection PyPropertyAccess - self.mock_di.chat_config_crud = Mock(spec = ChatConfigCRUD) - # noinspection PyPropertyAccess self.mock_di.telegram_bot_sdk = Mock(spec = TelegramBotSDK) # noinspection PyPropertyAccess self.mock_di.authorization_service = Mock() diff --git a/test/features/chat/membership/test_chat_membership_repo.py b/test/features/chat/membership/test_chat_membership_repo.py index a856c5c3..4420a5e4 100644 --- a/test/features/chat/membership/test_chat_membership_repo.py +++ b/test/features/chat/membership/test_chat_membership_repo.py @@ -5,8 +5,8 @@ from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfigSave from db.schema.user import UserSave +from features.chat.config.chat_config import ChatConfig from features.chat.membership.chat_membership import ChatMembership from features.chat.membership.chat_membership_repo import ChatMembershipRepository @@ -19,8 +19,8 @@ class ChatMembershipRepoTest(unittest.TestCase): def setUp(self): self.sql = SQLUtil() self.repo = self.sql.chat_membership_repo() - self.chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + self.chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) self.user = self.sql.user_crud().create( UserSave( @@ -123,8 +123,8 @@ def test_save_upserts_existing_membership(self): self.assertEqual(fetched.max_output_tokens, 8000) def test_get_all_for_user_returns_memberships(self): - second_chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat2", chat_type = ChatConfigDB.ChatType.telegram), + second_chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat2", chat_type = ChatConfigDB.ChatType.telegram), ) self.repo.save(ChatMembership( user_id = self.user.id, diff --git a/test/features/chat/membership/test_chat_membership_service.py b/test/features/chat/membership/test_chat_membership_service.py index 4b361e79..33ba49a7 100644 --- a/test/features/chat/membership/test_chat_membership_service.py +++ b/test/features/chat/membership/test_chat_membership_service.py @@ -7,9 +7,9 @@ from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfig, ChatConfigSave from db.schema.user import User, UserSave from di.di import DI +from features.chat.config.chat_config import ChatConfig from features.chat.membership.chat_membership import ChatMembership from features.chat.membership.chat_membership_service import ChatMembershipService from features.integrations.platform_bot_sdk import ChatAccess @@ -40,13 +40,11 @@ def setUp(self): ), ), ) - self.chat = ChatConfig.model_validate( - self.sql.chat_config_crud().create( - ChatConfigSave( - external_id = "chat_ext_1", - chat_type = ChatConfigDB.ChatType.telegram, - is_private = True, - ), + self.chat = self.sql.chat_config_repo().save( + ChatConfig( + external_id = "chat_ext_1", + chat_type = ChatConfigDB.ChatType.telegram, + is_private = True, ), ) self.mock_sdk = Mock() @@ -97,9 +95,10 @@ def test_get_all_for_user_returns_empty_when_none(self): self.assertEqual(len(result), 0) def test_get_all_for_user_returns_all_rows(self): - second_chat = ChatConfig.model_validate( - self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat_ext_2", chat_type = ChatConfigDB.ChatType.telegram), + second_chat = self.sql.chat_config_repo().save( + ChatConfig( + external_id = "chat_ext_2", + chat_type = ChatConfigDB.ChatType.telegram, ), ) repo = self.sql.chat_membership_repo() @@ -338,9 +337,10 @@ def test_refresh_chat_memberships_creates_missing_admin_row_with_defaults(self): self.assertTrue(result[0].use_custom_prompt) def test_refresh_chat_memberships_handles_multiple_chats(self): - second_chat = ChatConfig.model_validate( - self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat_ext_3", chat_type = ChatConfigDB.ChatType.telegram), + second_chat = self.sql.chat_config_repo().save( + ChatConfig( + external_id = "chat_ext_3", + chat_type = ChatConfigDB.ChatType.telegram, ), ) self.sql.chat_membership_repo().save( diff --git a/test/features/chat/telegram/test_currency_alert_responder.py b/test/features/chat/telegram/test_currency_alert_responder.py index d39be3e1..a4a4e0d3 100644 --- a/test/features/chat/telegram/test_currency_alert_responder.py +++ b/test/features/chat/telegram/test_currency_alert_responder.py @@ -3,7 +3,6 @@ from unittest.mock import Mock from uuid import UUID -from db.crud.chat_config import ChatConfigCRUD from db.crud.price_alert import PriceAlertCRUD from db.crud.sponsorship import SponsorshipCRUD from db.crud.tools_cache import ToolsCacheCRUD @@ -11,6 +10,8 @@ from db.model.chat_config import ChatConfigDB from di.di import DI from features.announcements.sys_announcements_service import SysAnnouncementsService +from features.chat.config.chat_config import ChatConfig +from features.chat.config.chat_config_repo import ChatConfigRepository from features.chat.currency_alert_service import DATETIME_PRINT_FORMAT, CurrencyAlertService from features.chat.telegram.currency_alert_responder import respond_with_currency_alerts from features.chat.telegram.sdk.telegram_bot_api import TelegramBotAPI @@ -33,21 +34,8 @@ def setUp(self): # noinspection PyPropertyAccess self.mock_di.user_crud = Mock(spec = UserCRUD) - # Set up chat_config_crud to return proper ChatConfigDB objects - # This is used in currency_alert_responder.py:21 - self.mock_di.chat_config_crud = Mock(spec = ChatConfigCRUD) - self.mock_di.chat_config_crud.get = lambda chat_id: ChatConfigDB( - chat_id = chat_id, - external_id = str(chat_id.int), - title = "Test Chat", - is_private = False, - reply_chance_percent = 100, - release_notifications = ChatConfigDB.ReleaseNotifications.all, - language_name = "English", - language_iso_code = "en", - media_mode = ChatConfigDB.MediaMode.photo, - chat_type = ChatConfigDB.ChatType.telegram, - ) + self.mock_di.chat_config_repo = Mock(spec = ChatConfigRepository) + self.mock_di.chat_config_repo.get = self.__make_chat_config # noinspection PyPropertyAccess self.mock_di.price_alert_crud = Mock(spec = PriceAlertCRUD) @@ -68,19 +56,6 @@ def setUp(self): self.mock_scoped_di = Mock() # Set up scoped DI dependencies - self.mock_scoped_di.chat_config_crud = Mock() - self.mock_scoped_di.chat_config_crud.get = lambda chat_id: ChatConfigDB( - chat_id = chat_id, - external_id = str(chat_id.int), - title = "Test Chat", - is_private = False, - reply_chance_percent = 100, - release_notifications = ChatConfigDB.ReleaseNotifications.all, - language_name = "English", - language_iso_code = "en", - media_mode = ChatConfigDB.MediaMode.photo, - chat_type = ChatConfigDB.ChatType.telegram, - ) # noinspection PyPropertyAccess self.mock_scoped_di.translations_cache = Mock(spec = TranslationsCache) @@ -99,6 +74,21 @@ def setUp(self): # Configure clone to return the same scoped_di self.mock_di.clone.return_value = self.mock_scoped_di + @staticmethod + def __make_chat_config(chat_id: UUID) -> ChatConfig: + return ChatConfig( + chat_id = chat_id, + external_id = str(chat_id.int), + title = "Test Chat", + is_private = False, + reply_chance_percent = 100, + release_notifications = ChatConfigDB.ReleaseNotifications.all, + language_name = "English", + language_iso_code = "en", + media_mode = ChatConfigDB.MediaMode.photo, + chat_type = ChatConfigDB.ChatType.telegram, + ) + # noinspection PyUnusedLocal def test_successful_announcements(self): # Create actual TriggeredAlert objects diff --git a/test/features/chat/telegram/test_domain_langchain_mapper.py b/test/features/chat/telegram/test_domain_langchain_mapper.py index c1db26f4..911a12ba 100644 --- a/test/features/chat/telegram/test_domain_langchain_mapper.py +++ b/test/features/chat/telegram/test_domain_langchain_mapper.py @@ -5,9 +5,9 @@ from langchain_core.messages import AIMessage, HumanMessage from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig from db.schema.chat_message import ChatMessage from db.schema.user import User, UserSave +from features.chat.config.chat_config import ChatConfig from features.chat.telegram.domain_langchain_mapper import DomainLangchainMapper, _split_preserving_blocks from features.integrations.integrations import resolve_agent_user from features.prompting.prompt_library import CHAT_MESSAGE_DELIMITER diff --git a/test/features/chat/telegram/test_release_summary_responder.py b/test/features/chat/telegram/test_release_summary_responder.py index 95c8d892..93ae242f 100644 --- a/test/features/chat/telegram/test_release_summary_responder.py +++ b/test/features/chat/telegram/test_release_summary_responder.py @@ -8,15 +8,15 @@ from langchain_core.messages import AIMessage from api.model.release_output_payload import ReleaseOutputPayload -from db.crud.chat_config import ChatConfigCRUD 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.chat_config import ChatConfig from db.schema.user import UserSave from di.di import DI from features.announcements.release_summary_service import ReleaseSummaryService +from features.chat.config.chat_config import ChatConfig +from features.chat.config.chat_config_repo import ChatConfigRepository # noinspection PyProtectedMember from features.chat.telegram.release_summary_responder import ( @@ -47,7 +47,7 @@ def setUp(self): # noinspection PyPropertyAccess self.mock_di.user_crud = Mock(spec = UserCRUD) # noinspection PyPropertyAccess - self.mock_di.chat_config_crud = Mock(spec = ChatConfigCRUD) + self.mock_di.chat_config_repo = Mock(spec = ChatConfigRepository) # noinspection PyPropertyAccess self.mock_di.sponsorship_crud = Mock(spec = SponsorshipCRUD) # noinspection PyPropertyAccess @@ -165,7 +165,7 @@ def test_version_match(self, mock_config): mock_summary_service = Mock(spec = ReleaseSummaryService) mock_summary_service.execute.return_value = Mock(content = "Test summary") self.mock_di.release_summary_service.return_value = mock_summary_service - self.mock_di.chat_config_crud.get_all.return_value = [] + self.mock_di.chat_config_repo.get_all.return_value = [] result = respond_with_summary(self.payload, self.mock_di) self.assertEqual(result["summaries_created"], 1) self.assertNotIn("Skipping", result["summary"]) @@ -185,7 +185,7 @@ def test_successful_summary(self, mock_config): # Use the real translations cache - it will cache summaries as needed # Mock chat config - self.mock_di.chat_config_crud.get_all.return_value = [self.__make_chat_db()] + self.mock_di.chat_config_repo.get_all.return_value = [self.__make_chat()] # Mock scoped DI and platform SDK for cloning mock_scoped_di = Mock() @@ -204,9 +204,9 @@ def test_multiple_languages(self, mock_config): mock_summarizer = Mock(spec = ReleaseSummaryService) mock_summarizer.execute.return_value = AIMessage(content = "Summary") self.mock_di.release_summary_service.return_value = mock_summarizer - self.mock_di.chat_config_crud.get_all.return_value = [ - self.__make_chat_db(chat_id = "123", lang_name = "English", lang_iso = "en"), - self.__make_chat_db(chat_id = "456", lang_name = "Spanish", lang_iso = "es"), + self.mock_di.chat_config_repo.get_all.return_value = [ + self.__make_chat(chat_id = "123", lang_name = "English", lang_iso = "en"), + self.__make_chat(chat_id = "456", lang_name = "Spanish", lang_iso = "es"), ] result = respond_with_summary(self.payload, self.mock_di) self.assertEqual(result["chats_notified"], 2) @@ -224,7 +224,7 @@ def test_telegram_send_failure(self): # Use the real translations cache # Mock chat config - self.mock_di.chat_config_crud.get_all.return_value = [self.__make_chat_db()] + self.mock_di.chat_config_repo.get_all.return_value = [self.__make_chat()] # Mock scoped DI with platform SDK send failure mock_scoped_di = Mock() @@ -248,7 +248,7 @@ def test_no_eligible_chats(self): # Use the real translations cache # Mock empty chat config list - self.mock_di.chat_config_crud.get_all.return_value = [] + self.mock_di.chat_config_repo.get_all.return_value = [] result = respond_with_summary(self.payload, self.mock_di) self.assertEqual(result["chats_eligible"], 0) @@ -259,12 +259,12 @@ def test_all_translations(self, mock_config): mock_sum = Mock(spec = ReleaseSummaryService) mock_sum.execute.return_value = Mock(content = "Gen summary") self.mock_di.release_summary_service.return_value = mock_sum - self.mock_di.chat_config_crud.get_all.return_value = [ - self.__make_chat_db(chat_id = "123", lang_name = "English", lang_iso = "en"), - self.__make_chat_db(chat_id = "456", lang_name = "Spanish", lang_iso = "es"), - self.__make_chat_db(chat_id = "789", lang_name = "Greek", lang_iso = "gr"), - self.__make_chat_db(chat_id = "sss", lang_name = "Spanish", lang_iso = "es"), - self.__make_chat_db(chat_id = "eee", lang_name = "English", lang_iso = "en"), + self.mock_di.chat_config_repo.get_all.return_value = [ + self.__make_chat(chat_id = "123", lang_name = "English", lang_iso = "en"), + self.__make_chat(chat_id = "456", lang_name = "Spanish", lang_iso = "es"), + self.__make_chat(chat_id = "789", lang_name = "Greek", lang_iso = "gr"), + self.__make_chat(chat_id = "sss", lang_name = "Spanish", lang_iso = "es"), + self.__make_chat(chat_id = "eee", lang_name = "English", lang_iso = "en"), ] result = respond_with_summary(self.payload, self.mock_di) self.assertEqual(result["chats_eligible"], 5) @@ -281,7 +281,7 @@ def test_summarization_failure(self): self.mock_di.release_summary_service.return_value = mock_summary_service # Mock chat config - self.mock_di.chat_config_crud.get_all.return_value = [self.__make_chat_db()] + self.mock_di.chat_config_repo.get_all.return_value = [self.__make_chat()] result = respond_with_summary(self.payload, self.mock_di) self.assertEqual(result["chats_notified"], 0) @@ -299,14 +299,14 @@ def test_strip_title_formatting(self): self.assertEqual(_strip_title_formatting("### "), "") @staticmethod - def __make_chat_db( + def __make_chat( notifications: ChatConfigDB.ReleaseNotifications = ChatConfigDB.ReleaseNotifications.all, media_mode: ChatConfigDB.MediaMode = ChatConfigDB.MediaMode.photo, chat_id: str = "1234", lang_name: str = "English", lang_iso: str = "en", - ) -> ChatConfigDB: - return ChatConfigDB( + ) -> ChatConfig: + return ChatConfig( chat_id = UUID(int = 1), external_id = chat_id, language_name = lang_name, @@ -318,26 +318,3 @@ def __make_chat_db( media_mode = media_mode, chat_type = ChatConfigDB.ChatType.telegram, ) - - @staticmethod - def __make_chat( - notifications: ChatConfigDB.ReleaseNotifications = ChatConfigDB.ReleaseNotifications.all, - media_mode: ChatConfigDB.MediaMode = ChatConfigDB.MediaMode.photo, - chat_id: str = "1234", - lang_name: str = "English", - lang_iso: str = "en", - ) -> ChatConfig: - return ChatConfig.model_validate( - ChatConfigDB( - chat_id = UUID(int = 1), - external_id = chat_id, - language_name = lang_name, - language_iso_code = lang_iso, - title = "Chat Title", - is_private = True, - reply_chance_percent = 100, - release_notifications = notifications, - media_mode = media_mode, - chat_type = ChatConfigDB.ChatType.telegram, - ), - ) diff --git a/test/features/chat/telegram/test_telegram_data_resolver.py b/test/features/chat/telegram/test_telegram_data_resolver.py index 42fe56a6..6a724c4d 100644 --- a/test/features/chat/telegram/test_telegram_data_resolver.py +++ b/test/features/chat/telegram/test_telegram_data_resolver.py @@ -1,18 +1,18 @@ import unittest from datetime import datetime, timedelta from unittest.mock import MagicMock, Mock, patch -from uuid import UUID from db.sql_util import SQLUtil from pydantic import SecretStr from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfig, ChatConfigSave from db.schema.chat_message import ChatMessage, ChatMessageSave from db.schema.chat_message_attachment import ChatMessageAttachment, ChatMessageAttachmentSave from db.schema.user import User, UserSave from di.di import DI +from features.chat.config.chat_config import ChatConfig +from features.chat.config.chat_config_remote_data import ChatConfigRemoteData from features.chat.telegram.sdk.telegram_bot_sdk import TelegramBotSDK from features.chat.telegram.telegram_data_resolver import TelegramDataResolver from features.chat.telegram.telegram_domain_mapper import TelegramDomainMapper @@ -32,7 +32,7 @@ def setUp(self): self.sql = SQLUtil() self.mock_di = Mock(spec = DI) # noinspection PyPropertyAccess - self.mock_di.chat_config_crud = self.sql.chat_config_crud() + self.mock_di.chat_config_repo = self.sql.chat_config_repo() # noinspection PyPropertyAccess self.mock_di.user_crud = self.sql.user_crud() # noinspection PyPropertyAccess @@ -53,7 +53,7 @@ def tearDown(self): self.sql.end_session() def test_resolve_no_author(self): - chat_config_data = ChatConfigSave( + chat_config_data = ChatConfigRemoteData( external_id = "c1", title = "Chat Title", is_private = True, @@ -92,7 +92,7 @@ def test_resolve_no_author(self): self.assertEqual(result.attachments[0].chat_id, result.chat.chat_id) def test_resolve_with_author_bot(self): - chat_config_data = ChatConfigSave( + chat_config_data = ChatConfigRemoteData( external_id = "c1", title = "Chat Title", is_private = True, @@ -140,7 +140,7 @@ def test_resolve_with_author_bot(self): self.mock_di.chat_membership_service.sync.assert_not_called() def test_resolve_with_author_normal(self): - chat_config_data = ChatConfigSave( + chat_config_data = ChatConfigRemoteData( external_id = "c1", title = "Chat Title", is_private = True, @@ -187,64 +187,6 @@ def test_resolve_with_author_normal(self): self.assertEqual(result.attachments[0].chat_id, result.chat.chat_id) self.mock_di.chat_membership_service.sync.assert_called_once() - def test_resolve_chat_config_existing(self): - existing_config_data = ChatConfigSave( - external_id = "c1", - language_iso_code = "en", - language_name = "English", - title = "Old Title", - is_private = False, - reply_chance_percent = 100, - release_notifications = ChatConfigDB.ReleaseNotifications.major, - media_mode = ChatConfigDB.MediaMode.file, # Non-default value to test preservation - chat_type = ChatConfigDB.ChatType.telegram, - ) - existing_config_db = self.sql.chat_config_crud().save(existing_config_data) - existing_config = ChatConfig.model_validate(existing_config_db) - - mapped_data = ChatConfigSave( - external_id = "c1", - title = "New Title", - is_private = True, - chat_type = ChatConfigDB.ChatType.telegram, - ) - - result = self.resolver.resolve_chat_config(mapped_data) - saved_config_db = self.sql.chat_config_crud().get(mapped_data.chat_id) - saved_config = ChatConfig.model_validate(saved_config_db) - - self.assertEqual(result, saved_config) - self.assertEqual(result.chat_id, mapped_data.chat_id) - self.assertEqual(result.language_iso_code, existing_config.language_iso_code) - self.assertEqual(result.language_name, existing_config.language_name) - self.assertEqual(result.title, mapped_data.title) - self.assertEqual(result.is_private, mapped_data.is_private) - self.assertEqual(result.reply_chance_percent, mapped_data.reply_chance_percent) - self.assertEqual(result.release_notifications, existing_config.release_notifications) - self.assertEqual(result.media_mode, existing_config.media_mode) - - def test_resolve_chat_config_new(self): - mapped_data = ChatConfigSave( - external_id = "c1", - title = "Title", - is_private = True, - chat_type = ChatConfigDB.ChatType.telegram, - ) - - result = self.resolver.resolve_chat_config(mapped_data) - saved_config_db = self.sql.chat_config_crud().get(result.chat_id) - saved_config = ChatConfig.model_validate(saved_config_db) - - self.assertEqual(result, saved_config) - # For new configs, chat_id is generated; mapped_data.chat_id remains None - self.assertIsNone(mapped_data.chat_id) - self.assertEqual(result.language_iso_code, mapped_data.language_iso_code) - self.assertEqual(result.language_name, mapped_data.language_name) - self.assertEqual(result.title, mapped_data.title) - self.assertEqual(result.is_private, mapped_data.is_private) - self.assertEqual(result.reply_chance_percent, mapped_data.reply_chance_percent) - self.assertEqual(result.release_notifications, mapped_data.release_notifications) - def test_resolve_author_none(self): result = self.resolver.resolve_author(None) self.assertIsNone(result) @@ -434,96 +376,9 @@ def test_resolve_author_preserves_name_when_empty(self): self.assertEqual(result.id, existing_user.id) self.assertEqual(result.full_name, existing_user.full_name) # Should preserve existing name - @patch("db.crud.user.UserCRUD.get_by_telegram_user_id") - @patch("db.crud.user.UserCRUD.get_by_telegram_username") - def test_resolve_author_api_key_reset(self, mock_get_by_username, mock_get_by_user_id): - fake_user = User( - id = UUID("123e4567-e89b-12d3-a456-426614174000"), - full_name = "Existing User", - telegram_username = "test_username", - telegram_chat_id = "c1", - telegram_user_id = 1, - open_ai_key = None, - anthropic_key = None, - google_ai_key = None, - perplexity_key = None, - replicate_key = None, - rapid_api_key = None, - coinmarketcap_key = None, - x_key = None, - x_ai_key = None, - group = UserDB.Group.developer, - created_at = datetime.now().date(), - ) - - mock_get_by_user_id.return_value = fake_user - mock_get_by_username.return_value = None - - # Get all API key fields dynamically - secret_fields = User._get_secret_str_fields() - - # test the no-key behavior for all API keys - mapped_data = UserSave( - telegram_user_id = 1, - full_name = "Test User", - telegram_chat_id = "c1", - ) - result = self.resolver.resolve_author(mapped_data) - for field in secret_fields: - self.assertIsNone(getattr(result, field), f"{field} should remain None when already None") - - # test the empty key behavior for all API keys - for field in secret_fields: - setattr(mapped_data, field, SecretStr("")) - setattr(fake_user, field, SecretStr("")) - result = self.resolver.resolve_author(mapped_data) - for field in secret_fields: - self.assertIsNone(getattr(result, field), f"{field} should be reset to None if empty") - - # test the whitespace behavior for all API keys - for field in secret_fields: - setattr(mapped_data, field, SecretStr(" ")) - setattr(fake_user, field, SecretStr(" ")) - result = self.resolver.resolve_author(mapped_data) - for field in secret_fields: - self.assertIsNone(getattr(result, field), f"{field} should be reset to None if whitespace") - - # test the valid key behavior for all API keys - for field in secret_fields: - setattr(mapped_data, field, SecretStr(f"valid_{field}")) - setattr(fake_user, field, SecretStr(f"valid_{field}")) - result = self.resolver.resolve_author(mapped_data) - for field in secret_fields: - result_key = getattr(result, field) - expected_key = result_key.get_secret_value() if result_key else None - self.assertEqual(expected_key, f"valid_{field}", f"{field} should remain unchanged if valid") - - def test_resolve_author_tool_choice_cleanup(self): - mapped_data = UserSave( - telegram_user_id = 1, - full_name = "Test User", - telegram_chat_id = "c1", - # Test various empty/whitespace scenarios for tool choice fields - tool_choice_chat = "", # empty string - tool_choice_reasoning = " ", # whitespace - tool_choice_copywriting = "perplexity", # valid value - tool_choice_vision = None, # already None - ) - - result = self.resolver.resolve_author(mapped_data) - assert result is not None - # Empty string should be cleaned to None - self.assertIsNone(result.tool_choice_chat, "Empty tool_choice_chat should be reset to None") - # Whitespace should be cleaned to None - self.assertIsNone(result.tool_choice_reasoning, "Whitespace tool_choice_reasoning should be reset to None") - # Valid value should be preserved - self.assertEqual(result.tool_choice_copywriting, "perplexity", "Valid tool_choice_copywriting should be preserved") - # None should remain None - self.assertIsNone(result.tool_choice_vision, "None tool_choice_vision should remain None") - def test_resolve_chat_message_new(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "c1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "c1", chat_type = ChatConfigDB.ChatType.telegram), ) mapped_data = ChatMessageSave( chat_id = chat.chat_id, @@ -543,8 +398,8 @@ def test_resolve_chat_message_new(self): self.assertEqual(result.text, mapped_data.text) def test_resolve_chat_message_with_existing(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "c1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "c1", chat_type = ChatConfigDB.ChatType.telegram), ) old_message_data = ChatMessageSave( chat_id = chat.chat_id, @@ -577,8 +432,8 @@ def test_resolve_chat_message_with_existing(self): self.assertEqual(result.text, mapped_data.text) def test_resolve_chat_message_attachment_new(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "c1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "c1", chat_type = ChatConfigDB.ChatType.telegram), ) self.sql.chat_message_crud().create( ChatMessageSave(chat_id = chat.chat_id, message_id = "m1", text = "x"), @@ -608,8 +463,8 @@ def test_resolve_chat_message_attachment_new(self): self.assertEqual(result.mime_type, mapped_data.mime_type) def test_resolve_chat_message_attachment_existing(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "c1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "c1", chat_type = ChatConfigDB.ChatType.telegram), ) self.sql.chat_message_crud().create( ChatMessageSave(chat_id = chat.chat_id, message_id = "m1", text = "x"), diff --git a/test/features/chat/telegram/test_telegram_domain_mapper.py b/test/features/chat/telegram/test_telegram_domain_mapper.py index 8cbe32b2..f9c6ab2c 100644 --- a/test/features/chat/telegram/test_telegram_domain_mapper.py +++ b/test/features/chat/telegram/test_telegram_domain_mapper.py @@ -2,6 +2,7 @@ import unittest from datetime import datetime +from db.model.chat_config import ChatConfigDB from db.model.user import UserDB from features.chat.telegram.model.attachment.audio import Audio from features.chat.telegram.model.attachment.document import Document @@ -246,9 +247,8 @@ def test_map_chat_filled(self): self.assertEqual(result.external_id, "10") self.assertEqual(result.title, "First · @chat_username") self.assertIsNone(result.language_iso_code) - self.assertIsNone(result.language_name) self.assertTrue(result.is_private) - self.assertEqual(result.reply_chance_percent, 100) + self.assertEqual(result.chat_type, ChatConfigDB.ChatType.telegram) def test_map_chat_empty(self): # 'from' is a reserved keyword in Python, so we use a workaround to access it @@ -267,9 +267,8 @@ def test_map_chat_empty(self): self.assertEqual(result.external_id, "10") self.assertEqual(result.title, "#10") self.assertEqual(result.language_iso_code, "de") - self.assertIsNone(result.language_name) self.assertFalse(result.is_private) - self.assertEqual(result.reply_chance_percent, 100) + self.assertEqual(result.chat_type, ChatConfigDB.ChatType.telegram) def test_resolve_chat_name_filled(self): result = self.mapper.resolve_chat_name( diff --git a/test/features/chat/telegram/test_telegram_progress_notifier.py b/test/features/chat/telegram/test_telegram_progress_notifier.py index fdb76c36..ece6e265 100644 --- a/test/features/chat/telegram/test_telegram_progress_notifier.py +++ b/test/features/chat/telegram/test_telegram_progress_notifier.py @@ -3,8 +3,8 @@ from uuid import UUID from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig from features.chat.chat_progress_notifier import ChatProgressNotifier +from features.chat.config.chat_config import ChatConfig from features.integrations.platform_bot_sdk import PlatformBotSDK diff --git a/test/features/chat/telegram/test_telegram_update_responder.py b/test/features/chat/telegram/test_telegram_update_responder.py index 0b7f7093..0c2fb4c5 100644 --- a/test/features/chat/telegram/test_telegram_update_responder.py +++ b/test/features/chat/telegram/test_telegram_update_responder.py @@ -8,9 +8,9 @@ from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfig from db.schema.chat_message import ChatMessage, ChatMessageSave from db.schema.user import User +from features.chat.config.chat_config import ChatConfig from features.chat.telegram.model.update import Update from features.chat.telegram.telegram_data_resolver import TelegramDataResolver from features.chat.telegram.telegram_domain_mapper import TelegramDomainMapper @@ -22,12 +22,17 @@ class TelegramUpdateResponderTest(unittest.TestCase): sql: SQLUtil update: Update di: Mock + mock_sleep: Mock def setUp(self): # create all the mocks self.sql = SQLUtil() self.update = Mock(spec = Update) + patcher_sleep = patch("features.chat.telegram.telegram_update_responder.sleep") + self.addCleanup(patcher_sleep.stop) + self.mock_sleep = patcher_sleep.start() + # mock the DI container patcher_di = patch("features.chat.telegram.telegram_update_responder.DI") self.addCleanup(patcher_di.stop) @@ -102,6 +107,7 @@ def test_successful_response(self): # Agent user creation logic was removed, so user_crud.save should not be called self.di.chat_agent.return_value.execute.assert_called_once() self.di.telegram_bot_sdk.send_text_message.assert_called_once_with("123", "Test response") + self.mock_sleep.assert_called_once_with(0.1) def test_reaction_response(self): self.di.chat_agent.return_value.execute.return_value = Mock(spec = AIMessage, content = "👍") @@ -133,6 +139,7 @@ def test_reaction_response(self): self.di.platform_bot_sdk.return_value.set_reaction.assert_called_once_with("123", "test-message-id", "👍") self.di.domain_langchain_mapper.map_bot_message_to_storage.assert_not_called() self.di.telegram_bot_sdk.send_text_message.assert_not_called() + self.mock_sleep.assert_not_called() saved_message = self.di.chat_message_crud.save.call_args.args[0] self.assertIsInstance(saved_message, ChatMessageSave) self.assertEqual(saved_message.message_id, "reaction:test-message-id") @@ -169,6 +176,7 @@ def test_reaction_response_failure(self): self.di.platform_bot_sdk.return_value.set_reaction.assert_called_once_with("123", "test-message-id", "👍") self.di.domain_langchain_mapper.map_bot_message_to_storage.assert_not_called() self.di.telegram_bot_sdk.send_text_message.assert_not_called() + self.mock_sleep.assert_not_called() saved_message = self.di.chat_message_crud.save.call_args.args[0] self.assertIsInstance(saved_message, ChatMessageSave) self.assertEqual(saved_message.message_id, "reaction:test-message-id") @@ -245,6 +253,7 @@ def test_general_exception(self): mock_error.return_value = "Error response" result = respond_to_update(self.update) - self.assertFalse(result) - self.di.telegram_bot_sdk.send_text_message.assert_called_once_with("123", "Error response") - self.di.chat_message_crud.save.assert_not_called() + self.assertFalse(result) + self.di.telegram_bot_sdk.send_text_message.assert_called_once_with("123", "Error response") + self.mock_sleep.assert_called_once_with(0.1) + self.di.chat_message_crud.save.assert_not_called() diff --git a/test/features/chat/test_chat_agent.py b/test/features/chat/test_chat_agent.py index eb823f48..1ad0c1d2 100644 --- a/test/features/chat/test_chat_agent.py +++ b/test/features/chat/test_chat_agent.py @@ -10,13 +10,13 @@ from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfig from db.schema.chat_message import ChatMessage from db.schema.user import User, UserSave from di.di import DI from features.chat.chat_agent import ChatAgent from features.chat.chat_progress_notifier import ChatProgressNotifier from features.chat.command_processor import CommandProcessor +from features.chat.config.chat_config import ChatConfig from features.chat.llm_tools.llm_tool_library import LLMToolLibrary from features.external_tools.tool_choice_resolver import ConfiguredTool from features.integrations.integrations import resolve_agent_user diff --git a/test/features/chat/test_chat_attachment_processor.py b/test/features/chat/test_chat_attachment_processor.py index cadb90fa..2c28aeb0 100644 --- a/test/features/chat/test_chat_attachment_processor.py +++ b/test/features/chat/test_chat_attachment_processor.py @@ -8,7 +8,6 @@ from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfig from db.schema.chat_message_attachment import ChatMessageAttachment from db.schema.tools_cache import ToolsCache from db.schema.user import User @@ -50,12 +49,10 @@ def setUp(self): self.mock_di = MagicMock() self.mock_cache_crud = MagicMock() self.mock_user_crud = MagicMock() - self.mock_chat_config_crud = MagicMock() self.mock_chat_message_attachment_crud = MagicMock() self.mock_access_token_resolver = MagicMock() self.mock_di.tools_cache_crud = self.mock_cache_crud self.mock_di.user_crud = self.mock_user_crud - self.mock_di.chat_config_crud = self.mock_chat_config_crud self.mock_di.chat_message_attachment_crud = self.mock_chat_message_attachment_crud self.mock_di.access_token_resolver = self.mock_access_token_resolver self.mock_di.invoker_chat_id = UUID(int = 1).hex @@ -89,17 +86,9 @@ def setUp(self): group = UserDB.Group.standard, created_at = datetime.now().date(), ) - self.chat_config = ChatConfig( - chat_id = UUID(int = 1), - external_id = "1", - language_name = "Spanish", - language_iso_code = "es", - chat_type = ChatConfigDB.ChatType.telegram, - ) self.attachment = _make_attachment() self.mock_user_crud.get.return_value = self.invoker_user.model_dump() - self.mock_chat_config_crud.get.return_value = self.chat_config.model_dump() self.mock_chat_message_attachment_crud.get.return_value = self.attachment.model_dump() self.mock_chat_message_attachment_crud.save.return_value = self.attachment.model_dump() self.mock_cache_crud.create_key.return_value = "test_cache_key" diff --git a/test/features/chat/test_command_processor.py b/test/features/chat/test_command_processor.py index 4efee529..3d5c1b91 100644 --- a/test/features/chat/test_command_processor.py +++ b/test/features/chat/test_command_processor.py @@ -8,7 +8,6 @@ from db.crud.user import UserCRUD from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfig from db.schema.user import User, UserSave from di.di import DI from features.chat.command_processor import ( @@ -19,6 +18,7 @@ CommandProcessor, is_known_command, ) +from features.chat.config.chat_config import ChatConfig from features.connect.profile_connect_service import ProfileConnectService from features.integrations.integrations import resolve_agent_user from features.integrations.platform_bot_sdk import PlatformBotSDK diff --git a/test/features/chat/test_currency_alert_service.py b/test/features/chat/test_currency_alert_service.py index bce75e79..27de7b29 100644 --- a/test/features/chat/test_currency_alert_service.py +++ b/test/features/chat/test_currency_alert_service.py @@ -5,17 +5,16 @@ from pydantic import SecretStr -from db.crud.chat_config import ChatConfigCRUD 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 from db.model.user import UserDB -from db.schema.chat_config import ChatConfig from db.schema.price_alert import PriceAlert from db.schema.user import User from di.di import DI +from features.chat.config.chat_config import ChatConfig 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 @@ -24,7 +23,6 @@ class CurrencyAlertServiceTest(unittest.TestCase): mock_user_dao: UserCRUD - mock_chat_config_dao: ChatConfigCRUD mock_price_alert_dao: PriceAlertCRUD mock_tools_cache_dao: ToolsCacheCRUD mock_sponsorship_dao: SponsorshipCRUD @@ -44,7 +42,6 @@ def setUp(self): self.mock_di.authorization_service = MagicMock() 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.chat_config_crud = self.mock_chat_config_dao = MagicMock(spec = ChatConfigCRUD) 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.telegram_bot_sdk = self.mock_telegram_bot_sdk = MagicMock(spec = TelegramBotSDK) diff --git a/test/features/chat/test_dev_announcements_service.py b/test/features/chat/test_dev_announcements_service.py index e38728ff..adcf8683 100644 --- a/test/features/chat/test_dev_announcements_service.py +++ b/test/features/chat/test_dev_announcements_service.py @@ -9,6 +9,7 @@ from db.model.chat_config import ChatConfigDB from db.model.user import UserDB from db.schema.user import User +from features.chat.config.chat_config import ChatConfig from features.chat.dev_announcements_service import DevAnnouncementsService from features.external_tools.tool_choice_resolver import ConfiguredTool from util.errors import AuthorizationError, NotFoundError @@ -47,9 +48,8 @@ def setUp(self): self.mock_di.platform_bot_sdk = MagicMock(return_value = self.mock_platform_sdk) self.mock_di.chat_langchain_model.return_value = MagicMock() self.mock_di.user_crud.get_by_telegram_username.return_value = None - self.mock_di.chat_config_crud.get.return_value = None - self.mock_di.chat_config_crud.get_by_external_identifiers.return_value = None - self.mock_di.chat_config_crud.get_all.return_value = [] + self.mock_di.chat_config_repo.get_by_external_identifiers.return_value = None + self.mock_di.chat_config_repo.get_all.return_value = [] self.mock_platform_sdk.send_text_message.return_value = {"result": {"message_id": 123}} self.mock_di.chat_message_crud.save.return_value = MagicMock() self.mock_di.translations_cache.get.return_value = "Translated announcement" @@ -62,7 +62,7 @@ def setUp(self): @staticmethod def __create_mock_chat_config(external_id: str, language: str = "en"): - return ChatConfigDB( + return ChatConfig( chat_id = UUID(int = 1), external_id = external_id, language_iso_code = language, @@ -110,7 +110,7 @@ def test_execute_success(self): mock_llm.invoke.return_value = AIMessage(content = "Refined announcement") self.mock_di.chat_langchain_model.return_value = mock_llm - self.mock_di.chat_config_crud.get_all.return_value = [ + self.mock_di.chat_config_repo.get_all.return_value = [ self.__create_mock_chat_config("1", "en"), self.__create_mock_chat_config("2", "es"), ] @@ -143,7 +143,7 @@ def test_execute_translation_failure(self): mock_llm.invoke.return_value = AIMessage(content = "Refined announcement") self.mock_di.chat_langchain_model.return_value = mock_llm - self.mock_di.chat_config_crud.get_all.return_value = [ + self.mock_di.chat_config_repo.get_all.return_value = [ self.__create_mock_chat_config("1", "en"), ] self.mock_di.translations_cache.get.return_value = None # Force translation attempt @@ -177,7 +177,7 @@ def test_execute_notification_failure(self): mock_llm.invoke.return_value = AIMessage(content = "Refined announcement") self.mock_di.chat_langchain_model.return_value = mock_llm - self.mock_di.chat_config_crud.get_all.return_value = [ + self.mock_di.chat_config_repo.get_all.return_value = [ self.__create_mock_chat_config("1", "en"), ] self.mock_platform_sdk.send_text_message.side_effect = Exception("Notification failed") @@ -210,7 +210,7 @@ def test_execute_no_chats(self): mock_llm.invoke.return_value = AIMessage(content = "Refined announcement") self.mock_di.chat_langchain_model.return_value = mock_llm - self.mock_di.chat_config_crud.get_all.return_value = [] + self.mock_di.chat_config_repo.get_all.return_value = [] # Mock external ID resolution from unittest.mock import patch @@ -257,7 +257,7 @@ def test_targeted_announcement_success(self): from unittest.mock import patch with patch("features.chat.dev_announcements_service.lookup_user_by_handle") as mock_lookup: mock_lookup.return_value = target_user - self.mock_di.chat_config_crud.get_by_external_identifiers.return_value = self.__create_mock_chat_config( + self.mock_di.chat_config_repo.get_by_external_identifiers.return_value = self.__create_mock_chat_config( external_id = "12345", language = "en", ) @@ -338,7 +338,7 @@ def test_targeted_announcement_chat_not_found(self): from unittest.mock import patch with patch("features.integrations.integrations.lookup_user_by_handle") as mock_lookup: mock_lookup.return_value = target_user - self.mock_di.chat_config_crud.get_by_external_identifiers.return_value = None + self.mock_di.chat_config_repo.get_by_external_identifiers.return_value = None with self.assertRaises(NotFoundError) as context: DevAnnouncementsService( diff --git a/test/features/chat/whatsapp/sdk/test_whatsapp_bot_sdk.py b/test/features/chat/whatsapp/sdk/test_whatsapp_bot_sdk.py index 6a4fe4e5..668b32c5 100644 --- a/test/features/chat/whatsapp/sdk/test_whatsapp_bot_sdk.py +++ b/test/features/chat/whatsapp/sdk/test_whatsapp_bot_sdk.py @@ -4,10 +4,10 @@ from uuid import UUID from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfig from db.schema.chat_message import ChatMessage from db.schema.chat_message_attachment import ChatMessageAttachment from di.di import DI +from features.chat.config.chat_config import ChatConfig from features.chat.whatsapp.model.response import ContactResponse, MessageResponse, SentMessageResponse from features.chat.whatsapp.sdk.whatsapp_bot_api import WhatsAppBotAPI from features.chat.whatsapp.sdk.whatsapp_bot_sdk import WhatsAppBotSDK @@ -31,7 +31,7 @@ def setUp(self): # noinspection PyPropertyAccess self.mock_di.whatsapp_domain_mapper = Mock(spec = WhatsAppDomainMapper) # noinspection PyPropertyAccess - self.mock_di.chat_config_crud = Mock() + self.mock_di.chat_config_repo = Mock() # noinspection PyPropertyAccess self.mock_di.chat_message_attachment_crud = Mock() # noinspection PyPropertyAccess @@ -58,11 +58,6 @@ def setUp(self): self.mock_di.whatsapp_bot_api.send_image.return_value = self.api_response self.mock_di.whatsapp_bot_api.send_document.return_value = self.api_response - # Mock ChatConfig that will be returned when looking up by external_id - mock_chat_config_db = Mock() - self.mock_di.chat_config_crud.get_by_external_identifiers.return_value = mock_chat_config_db - - # Create a real ChatConfig to be validated self.chat_config = ChatConfig( chat_id = self.chat_uuid, external_id = self.chat_id, @@ -70,16 +65,15 @@ def setUp(self): is_private = True, chat_type = ChatConfigDB.ChatType.whatsapp, ) + self.mock_di.chat_config_repo.get_by_external_identifiers.return_value = self.chat_config # Create a mock DB object that will be returned when saving message mock_message_db = Mock() self.mock_di.chat_message_crud.save.return_value = mock_message_db @patch.object(WhatsAppDomainMapper, "map_update") - @patch("db.schema.chat_config.ChatConfig.model_validate") @patch("db.schema.chat_message.ChatMessage.model_validate") - def test_send_text_message(self, mock_message_validate, mock_validate, mock_map_update): - mock_validate.return_value = self.chat_config + def test_send_text_message(self, mock_message_validate, mock_map_update): mock_message_validate.return_value = ChatMessage( message_id = self.message_id, chat_id = self.chat_uuid, @@ -110,10 +104,8 @@ def test_send_text_message(self, mock_message_validate, mock_validate, mock_map_ @patch("requests.get") @patch.object(WhatsAppDomainMapper, "map_update") - @patch("db.schema.chat_config.ChatConfig.model_validate") @patch("db.schema.chat_message.ChatMessage.model_validate") - def test_send_photo(self, mock_message_validate, mock_validate, mock_map_update, mock_requests_get): - mock_validate.return_value = self.chat_config + def test_send_photo(self, mock_message_validate, mock_map_update, mock_requests_get): mock_message_validate.return_value = ChatMessage( message_id = self.message_id, chat_id = self.chat_uuid, @@ -154,10 +146,8 @@ def test_send_photo(self, mock_message_validate, mock_validate, mock_map_update, @patch("requests.get") @patch.object(WhatsAppDomainMapper, "map_update") - @patch("db.schema.chat_config.ChatConfig.model_validate") @patch("db.schema.chat_message.ChatMessage.model_validate") - def test_send_document(self, mock_message_validate, mock_validate, mock_map_update, mock_requests_get): - mock_validate.return_value = self.chat_config + def test_send_document(self, mock_message_validate, mock_map_update, mock_requests_get): mock_message_validate.return_value = ChatMessage( message_id = self.message_id, chat_id = self.chat_uuid, @@ -207,10 +197,8 @@ def test_set_reaction(self): ) @patch.object(WhatsAppDomainMapper, "map_update") - @patch("db.schema.chat_config.ChatConfig.model_validate") @patch("db.schema.chat_message.ChatMessage.model_validate") - def test_send_button_link(self, mock_message_validate, mock_validate, mock_map_update): - mock_validate.return_value = self.chat_config + def test_send_button_link(self, mock_message_validate, mock_map_update): mock_message_validate.return_value = ChatMessage( message_id = self.message_id, chat_id = self.chat_uuid, @@ -276,10 +264,8 @@ def test_send_button_link(self, mock_message_validate, mock_validate, mock_map_u self.assertEqual(result.message_id, self.message_id) self.assertEqual(result.chat_id, self.chat_uuid) - @patch("db.schema.chat_config.ChatConfig.model_validate") @patch("db.schema.chat_message.ChatMessage.model_validate") - def test_store_api_response_mapping_failure(self, mock_message_validate, mock_validate): - mock_validate.return_value = self.chat_config + def test_store_api_response_mapping_failure(self, mock_message_validate): mock_message_validate.return_value = ChatMessage( message_id = self.message_id, chat_id = self.chat_uuid, @@ -295,10 +281,8 @@ def test_store_api_response_mapping_failure(self, mock_message_validate, mock_va ) self.assertIsInstance(result, ChatMessage) - @patch("db.schema.chat_config.ChatConfig.model_validate") @patch("db.schema.chat_message.ChatMessage.model_validate") - def test_store_api_response_resolution_failure(self, mock_message_validate, mock_validate): - mock_validate.return_value = self.chat_config + def test_store_api_response_resolution_failure(self, mock_message_validate): mock_message_validate.return_value = ChatMessage( message_id = self.message_id, chat_id = self.chat_uuid, diff --git a/test/features/chat/whatsapp/test_whatsapp_data_resolver.py b/test/features/chat/whatsapp/test_whatsapp_data_resolver.py index 951b6fff..09af41b3 100644 --- a/test/features/chat/whatsapp/test_whatsapp_data_resolver.py +++ b/test/features/chat/whatsapp/test_whatsapp_data_resolver.py @@ -1,18 +1,18 @@ import unittest from datetime import datetime, timedelta from unittest.mock import MagicMock, Mock, patch -from uuid import UUID from db.sql_util import SQLUtil from pydantic import SecretStr from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfig, ChatConfigSave from db.schema.chat_message import ChatMessage, ChatMessageSave from db.schema.chat_message_attachment import ChatMessageAttachment, ChatMessageAttachmentSave from db.schema.user import User, UserSave from di.di import DI +from features.chat.config.chat_config import ChatConfig +from features.chat.config.chat_config_remote_data import ChatConfigRemoteData from features.chat.whatsapp.sdk.whatsapp_bot_sdk import WhatsAppBotSDK from features.chat.whatsapp.whatsapp_data_resolver import WhatsAppDataResolver from features.chat.whatsapp.whatsapp_domain_mapper import WhatsAppDomainMapper @@ -32,7 +32,7 @@ def setUp(self): self.sql = SQLUtil() self.mock_di = Mock(spec = DI) # noinspection PyPropertyAccess - self.mock_di.chat_config_crud = self.sql.chat_config_crud() + self.mock_di.chat_config_repo = self.sql.chat_config_repo() # noinspection PyPropertyAccess self.mock_di.user_crud = self.sql.user_crud() # noinspection PyPropertyAccess @@ -53,7 +53,7 @@ def tearDown(self): self.sql.end_session() def test_resolve_no_author(self): - chat_config_data = ChatConfigSave( + chat_config_data = ChatConfigRemoteData( external_id = "c1", title = "Chat Title", is_private = True, @@ -92,7 +92,7 @@ def test_resolve_no_author(self): self.assertEqual(result.attachments[0].chat_id, result.chat.chat_id) def test_resolve_with_author_bot(self): - chat_config_data = ChatConfigSave( + chat_config_data = ChatConfigRemoteData( external_id = "c1", title = "Chat Title", is_private = True, @@ -137,7 +137,7 @@ def test_resolve_with_author_bot(self): self.mock_di.chat_membership_service.sync.assert_not_called() def test_resolve_with_author_normal(self): - chat_config_data = ChatConfigSave( + chat_config_data = ChatConfigRemoteData( external_id = "c1", title = "Chat Title", is_private = True, @@ -181,64 +181,6 @@ def test_resolve_with_author_normal(self): self.assertEqual(result.attachments[0].chat_id, result.chat.chat_id) self.mock_di.chat_membership_service.sync.assert_called_once() - def test_resolve_chat_config_existing(self): - existing_config_data = ChatConfigSave( - external_id = "c1", - language_iso_code = "en", - language_name = "English", - title = "Old Title", - is_private = False, - reply_chance_percent = 100, - release_notifications = ChatConfigDB.ReleaseNotifications.major, - media_mode = ChatConfigDB.MediaMode.all, # Non-default value to test preservation - chat_type = ChatConfigDB.ChatType.whatsapp, - ) - existing_config_db = self.sql.chat_config_crud().save(existing_config_data) - existing_config = ChatConfig.model_validate(existing_config_db) - - mapped_data = ChatConfigSave( - external_id = "c1", - title = "New Title", - is_private = True, - chat_type = ChatConfigDB.ChatType.whatsapp, - ) - - result = self.resolver.resolve_chat_config(mapped_data) - saved_config_db = self.sql.chat_config_crud().get(mapped_data.chat_id) - saved_config = ChatConfig.model_validate(saved_config_db) - - self.assertEqual(result, saved_config) - self.assertEqual(result.chat_id, mapped_data.chat_id) - self.assertEqual(result.language_iso_code, existing_config.language_iso_code) - self.assertEqual(result.language_name, existing_config.language_name) - self.assertEqual(result.title, mapped_data.title) - self.assertEqual(result.is_private, mapped_data.is_private) - self.assertEqual(result.reply_chance_percent, mapped_data.reply_chance_percent) - self.assertEqual(result.release_notifications, existing_config.release_notifications) - self.assertEqual(result.media_mode, existing_config.media_mode) - - def test_resolve_chat_config_new(self): - mapped_data = ChatConfigSave( - external_id = "c1", - title = "Title", - is_private = True, - chat_type = ChatConfigDB.ChatType.whatsapp, - ) - - result = self.resolver.resolve_chat_config(mapped_data) - saved_config_db = self.sql.chat_config_crud().get(result.chat_id) - saved_config = ChatConfig.model_validate(saved_config_db) - - self.assertEqual(result, saved_config) - # For new configs, chat_id is generated; mapped_data.chat_id remains None - self.assertIsNone(mapped_data.chat_id) - self.assertEqual(result.language_iso_code, mapped_data.language_iso_code) - self.assertEqual(result.language_name, mapped_data.language_name) - self.assertEqual(result.title, mapped_data.title) - self.assertEqual(result.is_private, mapped_data.is_private) - self.assertEqual(result.reply_chance_percent, mapped_data.reply_chance_percent) - self.assertEqual(result.release_notifications, mapped_data.release_notifications) - def test_resolve_author_none(self): result = self.resolver.resolve_author(None) self.assertIsNone(result) @@ -417,92 +359,9 @@ def test_resolve_author_preserves_name_when_empty(self): self.assertEqual(result.id, existing_user.id) self.assertEqual(result.full_name, existing_user.full_name) # Should preserve existing name - @patch("db.crud.user.UserCRUD.get_by_whatsapp_phone_number") - @patch("db.crud.user.UserCRUD.get_by_whatsapp_user_id") - def test_resolve_author_api_key_reset(self, mock_get_by_user_id, mock_get_by_phone): - fake_user = User( - id = UUID("123e4567-e89b-12d3-a456-426614174000"), - full_name = "Existing User", - whatsapp_user_id = "1", - open_ai_key = None, - anthropic_key = None, - google_ai_key = None, - perplexity_key = None, - replicate_key = None, - rapid_api_key = None, - coinmarketcap_key = None, - x_key = None, - x_ai_key = None, - group = UserDB.Group.developer, - created_at = datetime.now().date(), - ) - - mock_get_by_user_id.return_value = fake_user - mock_get_by_phone.return_value = None # Should not be called since user_id lookup succeeds - - # Get all API key fields dynamically - secret_fields = User._get_secret_str_fields() - - # test the no-key behavior for all API keys - mapped_data = UserSave( - whatsapp_user_id = "1", - full_name = "Test User", - ) - result = self.resolver.resolve_author(mapped_data) - for field in secret_fields: - self.assertIsNone(getattr(result, field), f"{field} should remain None when already None") - - # test the empty key behavior for all API keys - for field in secret_fields: - setattr(mapped_data, field, SecretStr("")) - setattr(fake_user, field, SecretStr("")) - result = self.resolver.resolve_author(mapped_data) - for field in secret_fields: - self.assertIsNone(getattr(result, field), f"{field} should be reset to None if empty") - - # test the whitespace behavior for all API keys - for field in secret_fields: - setattr(mapped_data, field, SecretStr(" ")) - setattr(fake_user, field, SecretStr(" ")) - result = self.resolver.resolve_author(mapped_data) - for field in secret_fields: - self.assertIsNone(getattr(result, field), f"{field} should be reset to None if whitespace") - - # test the valid key behavior for all API keys - for field in secret_fields: - setattr(mapped_data, field, SecretStr(f"valid_{field}")) - setattr(fake_user, field, SecretStr(f"valid_{field}")) - result = self.resolver.resolve_author(mapped_data) - for field in secret_fields: - result_key = getattr(result, field) - expected_key = result_key.get_secret_value() if result_key else None - self.assertEqual(expected_key, f"valid_{field}", f"{field} should remain unchanged if valid") - - def test_resolve_author_tool_choice_cleanup(self): - mapped_data = UserSave( - whatsapp_user_id = "1", - full_name = "Test User", - # Test various empty/whitespace scenarios for tool choice fields - tool_choice_chat = "", # empty string - tool_choice_reasoning = " ", # whitespace - tool_choice_copywriting = "perplexity", # valid value - tool_choice_vision = None, # already None - ) - - result = self.resolver.resolve_author(mapped_data) - assert result is not None - # Empty string should be cleaned to None - self.assertIsNone(result.tool_choice_chat, "Empty tool_choice_chat should be reset to None") - # Whitespace should be cleaned to None - self.assertIsNone(result.tool_choice_reasoning, "Whitespace tool_choice_reasoning should be reset to None") - # Valid value should be preserved - self.assertEqual(result.tool_choice_copywriting, "perplexity", "Valid tool_choice_copywriting should be preserved") - # None should remain None - self.assertIsNone(result.tool_choice_vision, "None tool_choice_vision should remain None") - def test_resolve_chat_message_new(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "c1", chat_type = ChatConfigDB.ChatType.whatsapp), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "c1", chat_type = ChatConfigDB.ChatType.whatsapp), ) mapped_data = ChatMessageSave( chat_id = chat.chat_id, @@ -522,8 +381,8 @@ def test_resolve_chat_message_new(self): self.assertEqual(result.text, mapped_data.text) def test_resolve_chat_message_with_existing(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "c1", chat_type = ChatConfigDB.ChatType.whatsapp), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "c1", chat_type = ChatConfigDB.ChatType.whatsapp), ) old_message_data = ChatMessageSave( chat_id = chat.chat_id, @@ -556,8 +415,8 @@ def test_resolve_chat_message_with_existing(self): self.assertEqual(result.text, mapped_data.text) def test_resolve_chat_message_attachment_new(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "c1", chat_type = ChatConfigDB.ChatType.whatsapp), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "c1", chat_type = ChatConfigDB.ChatType.whatsapp), ) self.sql.chat_message_crud().create( ChatMessageSave(chat_id = chat.chat_id, message_id = "m1", text = "x"), @@ -587,8 +446,8 @@ def test_resolve_chat_message_attachment_new(self): self.assertEqual(result.mime_type, mapped_data.mime_type) def test_resolve_chat_message_attachment_existing(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "c1", chat_type = ChatConfigDB.ChatType.whatsapp), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "c1", chat_type = ChatConfigDB.ChatType.whatsapp), ) self.sql.chat_message_crud().create( ChatMessageSave(chat_id = chat.chat_id, message_id = "m1", text = "x"), diff --git a/test/features/chat/whatsapp/test_whatsapp_update_responder.py b/test/features/chat/whatsapp/test_whatsapp_update_responder.py index 72a91b82..87ee9586 100644 --- a/test/features/chat/whatsapp/test_whatsapp_update_responder.py +++ b/test/features/chat/whatsapp/test_whatsapp_update_responder.py @@ -8,9 +8,9 @@ from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfig from db.schema.chat_message import ChatMessage, ChatMessageSave from db.schema.user import User +from features.chat.config.chat_config import ChatConfig from features.chat.whatsapp.model.update import Update from features.chat.whatsapp.whatsapp_data_resolver import WhatsAppDataResolver from features.chat.whatsapp.whatsapp_domain_mapper import WhatsAppDomainMapper @@ -22,12 +22,17 @@ class WhatsAppUpdateResponderTest(unittest.TestCase): sql: SQLUtil update: Update di: Mock + mock_sleep: Mock def setUp(self): # create all the mocks self.sql = SQLUtil() self.update = Update(object = "whatsapp_business_account", entry = []) + patcher_sleep = patch("features.chat.whatsapp.whatsapp_update_responder.sleep") + self.addCleanup(patcher_sleep.stop) + self.mock_sleep = patcher_sleep.start() + # mock the DI container patcher_di = patch("features.chat.whatsapp.whatsapp_update_responder.DI") self.addCleanup(patcher_di.stop) @@ -106,6 +111,7 @@ def test_successful_response(self): # Agent user creation logic was removed, so user_crud.save should not be called self.di.chat_agent.return_value.execute.assert_called_once() self.di.whatsapp_bot_sdk.send_text_message.assert_called_once_with("123", "Test response") + self.mock_sleep.assert_called_once_with(0.1) def test_reaction_response(self): self.di.chat_agent.return_value.execute.return_value = Mock(spec = AIMessage, content = "👍") @@ -146,6 +152,7 @@ def test_reaction_response(self): self.di.platform_bot_sdk.return_value.set_reaction.assert_called_once_with("123", "test-message-id", "👍") self.di.domain_langchain_mapper.map_bot_message_to_storage.assert_not_called() self.di.whatsapp_bot_sdk.send_text_message.assert_not_called() + self.mock_sleep.assert_not_called() self.di.whatsapp_bot_sdk.mark_as_read.assert_called_once_with("test-message-id") saved_message = self.di.chat_message_crud.save.call_args.args[0] self.assertIsInstance(saved_message, ChatMessageSave) @@ -192,6 +199,7 @@ def test_reaction_response_failure(self): self.di.platform_bot_sdk.return_value.set_reaction.assert_called_once_with("123", "test-message-id", "👍") self.di.domain_langchain_mapper.map_bot_message_to_storage.assert_not_called() self.di.whatsapp_bot_sdk.send_text_message.assert_not_called() + self.mock_sleep.assert_not_called() self.di.whatsapp_bot_sdk.mark_as_read.assert_called_once_with("test-message-id") saved_message = self.di.chat_message_crud.save.call_args.args[0] self.assertIsInstance(saved_message, ChatMessageSave) @@ -267,4 +275,5 @@ def test_general_exception(self): self.assertFalse(result) self.di.whatsapp_bot_sdk.send_text_message.assert_called_once_with("123", "Error response") + self.mock_sleep.assert_called_once_with(0.1) self.di.chat_message_crud.save.assert_not_called() diff --git a/test/features/integrations/test_integrations.py b/test/features/integrations/test_integrations.py index 5a99510c..179feeb2 100644 --- a/test/features/integrations/test_integrations.py +++ b/test/features/integrations/test_integrations.py @@ -5,15 +5,15 @@ from pydantic import SecretStr -from db.crud.chat_config import ChatConfigCRUD from db.crud.chat_message import ChatMessageCRUD from db.crud.user import UserCRUD from db.model.chat_config import ChatConfigDB from db.model.chat_message import ChatMessageDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfig from db.schema.user import User, UserSave from di.di import DI +from features.chat.config.chat_config import ChatConfig +from features.chat.config.chat_config_repo import ChatConfigRepository from features.integrations.integrations import ( WHATSAPP_MESSAGING_WINDOW_HOURS, format_handle, @@ -833,7 +833,7 @@ class NotificationChatResolutionTest(TestCase): def setUp(self): self.mock_di = Mock(spec = DI) - self.mock_di.chat_config_crud = Mock(spec = ChatConfigCRUD) + self.mock_di.chat_config_repo = Mock(spec = ChatConfigRepository) self.mock_di.chat_message_crud = Mock(spec = ChatMessageCRUD) self.user = User( @@ -847,8 +847,8 @@ def setUp(self): group = UserDB.Group.standard, ) - def _make_chat(self, chat_type: ChatConfigDB.ChatType, external_id: str) -> ChatConfigDB: - return ChatConfigDB( + def _make_chat(self, chat_type: ChatConfigDB.ChatType, external_id: str) -> ChatConfig: + return ChatConfig( chat_id = UUID(int = hash(external_id) % (2 ** 32)), external_id = external_id, title = f"Test {chat_type.value} Chat", @@ -871,13 +871,13 @@ def _make_message(self, chat_id: UUID, author_id: UUID, sent_at: datetime) -> Ch ) def test_no_platforms_available(self): - self.mock_di.chat_config_crud.get_by_external_identifiers = Mock(return_value = None) + self.mock_di.chat_config_repo.get_by_external_identifiers = Mock(return_value = None) result = resolve_best_notification_chat(self.user, self.mock_di) self.assertIsNone(result) def test_telegram_only(self): telegram_chat = self._make_chat(ChatConfigDB.ChatType.telegram, "telegram_chat_123") - self.mock_di.chat_config_crud.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( + self.mock_di.chat_config_repo.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( telegram_chat if chat_type == ChatConfigDB.ChatType.telegram else None )) self.mock_di.chat_message_crud.get_latest_chat_messages = Mock(return_value = []) @@ -888,7 +888,7 @@ def test_telegram_only(self): def test_telegram_no_messages_still_selected(self): telegram_chat = self._make_chat(ChatConfigDB.ChatType.telegram, "telegram_chat_123") - self.mock_di.chat_config_crud.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( + self.mock_di.chat_config_repo.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( telegram_chat if chat_type == ChatConfigDB.ChatType.telegram else None )) self.mock_di.chat_message_crud.get_latest_chat_messages = Mock(return_value = []) @@ -901,7 +901,7 @@ def test_whatsapp_within_window(self): whatsapp_chat = self._make_chat(ChatConfigDB.ChatType.whatsapp, "whatsapp_user_123") recent_msg = self._make_message(whatsapp_chat.chat_id, self.user.id, datetime.now() - timedelta(hours = 12)) - self.mock_di.chat_config_crud.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( + self.mock_di.chat_config_repo.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( whatsapp_chat if chat_type == ChatConfigDB.ChatType.whatsapp else None )) self.mock_di.chat_message_crud.get_latest_chat_messages = Mock(return_value = [recent_msg]) @@ -917,7 +917,7 @@ def test_whatsapp_outside_window(self): datetime.now() - timedelta(hours = WHATSAPP_MESSAGING_WINDOW_HOURS + 1), ) - self.mock_di.chat_config_crud.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( + self.mock_di.chat_config_repo.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( whatsapp_chat if chat_type == ChatConfigDB.ChatType.whatsapp else None )) self.mock_di.chat_message_crud.get_latest_chat_messages = Mock(return_value = [old_msg]) @@ -931,7 +931,7 @@ def test_both_eligible_whatsapp_more_recent(self): telegram_msg = self._make_message(telegram_chat.chat_id, self.user.id, datetime.now() - timedelta(hours = 10)) whatsapp_msg = self._make_message(whatsapp_chat.chat_id, self.user.id, datetime.now() - timedelta(hours = 2)) - self.mock_di.chat_config_crud.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( + self.mock_di.chat_config_repo.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( telegram_chat if chat_type == ChatConfigDB.ChatType.telegram else whatsapp_chat if chat_type == ChatConfigDB.ChatType.whatsapp else None @@ -951,7 +951,7 @@ def test_both_eligible_telegram_more_recent(self): telegram_msg = self._make_message(telegram_chat.chat_id, self.user.id, datetime.now() - timedelta(hours = 2)) whatsapp_msg = self._make_message(whatsapp_chat.chat_id, self.user.id, datetime.now() - timedelta(hours = 10)) - self.mock_di.chat_config_crud.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( + self.mock_di.chat_config_repo.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( telegram_chat if chat_type == ChatConfigDB.ChatType.telegram else whatsapp_chat if chat_type == ChatConfigDB.ChatType.whatsapp else None @@ -974,7 +974,7 @@ def test_whatsapp_outside_window_telegram_available(self): datetime.now() - timedelta(hours = WHATSAPP_MESSAGING_WINDOW_HOURS + 2), ) - self.mock_di.chat_config_crud.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( + self.mock_di.chat_config_repo.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( telegram_chat if chat_type == ChatConfigDB.ChatType.telegram else whatsapp_chat if chat_type == ChatConfigDB.ChatType.whatsapp else None @@ -992,7 +992,7 @@ def test_non_private_chat_excluded(self): public_chat = self._make_chat(ChatConfigDB.ChatType.telegram, "telegram_chat_123") public_chat.is_private = False - self.mock_di.chat_config_crud.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( + self.mock_di.chat_config_repo.get_by_external_identifiers = Mock(side_effect = lambda external_id, chat_type: ( public_chat if chat_type == ChatConfigDB.ChatType.telegram else None )) diff --git a/test/features/integrations/test_platform_bot_sdk.py b/test/features/integrations/test_platform_bot_sdk.py index 19e3376d..e982cd4b 100644 --- a/test/features/integrations/test_platform_bot_sdk.py +++ b/test/features/integrations/test_platform_bot_sdk.py @@ -8,9 +8,9 @@ from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfig from db.schema.user import User from di.di import DI +from features.chat.config.chat_config import ChatConfig from features.integrations.platform_bot_sdk import ChatAccess, PlatformBotSDK From 3341cbca4a725b48520c0bb91191aa0e269312b9 Mon Sep 17 00:00:00 2001 From: Milos Marinkovic Date: Thu, 18 Jun 2026 15:32:49 +0200 Subject: [PATCH 5/7] Remove residue Chat Config legacy code --- src/db/crud/chat_config.py | 58 ------ src/db/schema/chat_config.py | 26 --- test/db/crud/test_chat_config.py | 173 ------------------ test/db/crud/test_chat_message.py | 42 ++--- test/db/crud/test_chat_message_attachment.py | 54 +++--- test/db/crud/test_price_alert.py | 42 ++--- test/db/sql_util.py | 6 - .../currencies/test_exchange_rate_fetcher.py | 3 - 8 files changed, 69 insertions(+), 335 deletions(-) delete mode 100644 src/db/crud/chat_config.py delete mode 100644 src/db/schema/chat_config.py delete mode 100644 test/db/crud/test_chat_config.py diff --git a/src/db/crud/chat_config.py b/src/db/crud/chat_config.py deleted file mode 100644 index 8875011d..00000000 --- a/src/db/crud/chat_config.py +++ /dev/null @@ -1,58 +0,0 @@ -from uuid import UUID - -from sqlalchemy.orm import Session - -from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfigSave - - -class ChatConfigCRUD: - - _db: Session - - def __init__(self, db: Session): - self._db = db - - def get(self, chat_id: UUID) -> ChatConfigDB | None: - return self._db.query(ChatConfigDB).filter( - ChatConfigDB.chat_id == chat_id, - ).first() - - def get_all(self, skip: int = 0, limit: int = 100) -> list[ChatConfigDB]: - # noinspection PyTypeChecker - return self._db.query(ChatConfigDB).offset(skip).limit(limit).all() - - def create(self, create_data: ChatConfigSave) -> ChatConfigDB: - chat_config = ChatConfigDB(**create_data.model_dump()) - self._db.add(chat_config) - self._db.commit() - self._db.refresh(chat_config) - return chat_config - - def get_by_external_identifiers(self, external_id: str, chat_type: ChatConfigDB.ChatType) -> ChatConfigDB | None: - return self._db.query(ChatConfigDB).filter( - ChatConfigDB.external_id == external_id, - ChatConfigDB.chat_type == chat_type, - ).first() - - def update(self, update_data: ChatConfigSave) -> ChatConfigDB | None: - chat_config = self.get(update_data.chat_id) - if chat_config: - for key, value in update_data.model_dump().items(): - setattr(chat_config, key, value) - self._db.commit() - self._db.refresh(chat_config) - return chat_config - - def save(self, data: ChatConfigSave) -> ChatConfigDB: - updated_config = self.update(data) - if updated_config: - return updated_config # available only if update was successful - return self.create(data) - - def delete(self, chat_id: UUID) -> ChatConfigDB | None: - chat_config = self.get(chat_id) - if chat_config: - self._db.delete(chat_config) - self._db.commit() - return chat_config diff --git a/src/db/schema/chat_config.py b/src/db/schema/chat_config.py deleted file mode 100644 index 58ee3a0a..00000000 --- a/src/db/schema/chat_config.py +++ /dev/null @@ -1,26 +0,0 @@ -from uuid import UUID - -from pydantic import BaseModel, ConfigDict - -from db.model.chat_config import ChatConfigDB - - -class ChatConfigBase(BaseModel): - external_id: str | None = None - language_iso_code: str | None = None - language_name: str | None = None - title: str | None = None - is_private: bool = False - reply_chance_percent: int = 100 - release_notifications: ChatConfigDB.ReleaseNotifications = ChatConfigDB.ReleaseNotifications.major - media_mode: ChatConfigDB.MediaMode = ChatConfigDB.MediaMode.photo - chat_type: ChatConfigDB.ChatType - - -class ChatConfigSave(ChatConfigBase): - chat_id: UUID | None = None - - -class ChatConfig(ChatConfigBase): - chat_id: UUID - model_config = ConfigDict(from_attributes = True) diff --git a/test/db/crud/test_chat_config.py b/test/db/crud/test_chat_config.py deleted file mode 100644 index 79bdbdf2..00000000 --- a/test/db/crud/test_chat_config.py +++ /dev/null @@ -1,173 +0,0 @@ -import unittest - -from db.sql_util import SQLUtil - -from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfigSave - - -class ChatConfigCRUDTest(unittest.TestCase): - - sql: SQLUtil - - def setUp(self): - self.sql = SQLUtil() - - def tearDown(self): - self.sql.end_session() - - def test_create_chat_config(self): - chat_config_data = ChatConfigSave( - external_id = "chat1", - language_iso_code = "en", - language_name = "English", - title = "Chat One", - is_private = True, - reply_chance_percent = 100, - release_notifications = ChatConfigDB.ReleaseNotifications.major, - media_mode = ChatConfigDB.MediaMode.photo, - chat_type = ChatConfigDB.ChatType.telegram, - ) - - chat_config = self.sql.chat_config_crud().create(chat_config_data) - - self.assertIsNotNone(chat_config.chat_id) - self.assertEqual(chat_config.external_id, chat_config_data.external_id) - self.assertEqual(chat_config.language_iso_code, chat_config_data.language_iso_code) - self.assertEqual(chat_config.language_name, chat_config_data.language_name) - self.assertEqual(chat_config.title, chat_config_data.title) - self.assertEqual(chat_config.is_private, chat_config_data.is_private) - self.assertEqual(chat_config.reply_chance_percent, chat_config_data.reply_chance_percent) - self.assertEqual(chat_config.release_notifications, chat_config_data.release_notifications) - self.assertEqual(chat_config.media_mode, chat_config_data.media_mode) - self.assertEqual(chat_config.chat_type, chat_config_data.chat_type) - - def test_get_chat_config(self): - chat_config_data = ChatConfigSave( - external_id = "chat1", - language_iso_code = "en", - language_name = "English", - title = "Chat One", - is_private = True, - reply_chance_percent = 100, - release_notifications = ChatConfigDB.ReleaseNotifications.major, - media_mode = ChatConfigDB.MediaMode.photo, - chat_type = ChatConfigDB.ChatType.telegram, - ) - created_chat_config = self.sql.chat_config_crud().create(chat_config_data) - - fetched_chat_config = self.sql.chat_config_crud().get(created_chat_config.chat_id) - - self.assertEqual(fetched_chat_config.chat_id, created_chat_config.chat_id) - - def test_get_all_chat_configs(self): - chat_configs = [ - self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), - ), - self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat2", chat_type = ChatConfigDB.ChatType.background), - ), - ] - - fetched_chat_configs = self.sql.chat_config_crud().get_all() - - self.assertEqual(len(fetched_chat_configs), len(chat_configs)) - for i in range(len(chat_configs)): - self.assertEqual(fetched_chat_configs[i].chat_id, chat_configs[i].chat_id) - - def test_update_chat_config(self): - chat_config_data = ChatConfigSave( - external_id = "chat1", - language_iso_code = "en", - language_name = "English", - title = "Chat One", - is_private = True, - reply_chance_percent = 100, - release_notifications = ChatConfigDB.ReleaseNotifications.major, - media_mode = ChatConfigDB.MediaMode.photo, - chat_type = ChatConfigDB.ChatType.telegram, - ) - created_chat_config = self.sql.chat_config_crud().create(chat_config_data) - - update_data = ChatConfigSave( - chat_id = created_chat_config.chat_id, - language_iso_code = "fr", - language_name = "French", - title = "Chat Another", - is_private = False, - reply_chance_percent = 0, - release_notifications = ChatConfigDB.ReleaseNotifications.minor, - media_mode = ChatConfigDB.MediaMode.file, - chat_type = ChatConfigDB.ChatType.telegram, - ) - updated_chat_config = self.sql.chat_config_crud().update(update_data) - - self.assertEqual(updated_chat_config.chat_id, created_chat_config.chat_id) - self.assertEqual(updated_chat_config.language_iso_code, update_data.language_iso_code) - self.assertEqual(updated_chat_config.language_name, update_data.language_name) - self.assertEqual(updated_chat_config.title, update_data.title) - self.assertEqual(updated_chat_config.is_private, update_data.is_private) - self.assertEqual(updated_chat_config.reply_chance_percent, update_data.reply_chance_percent) - self.assertEqual(updated_chat_config.media_mode, update_data.media_mode) - self.assertEqual(updated_chat_config.chat_type, update_data.chat_type) - - def test_save_chat_config(self): - chat_config_data = ChatConfigSave( - external_id = "chat1", - language_iso_code = "en", - language_name = "English", - title = "Chat One", - is_private = True, - reply_chance_percent = 100, - release_notifications = ChatConfigDB.ReleaseNotifications.major, - media_mode = ChatConfigDB.MediaMode.photo, - chat_type = ChatConfigDB.ChatType.telegram, - ) - - # First, save should create the record - saved_chat_config = self.sql.chat_config_crud().save(chat_config_data) - self.assertIsNotNone(saved_chat_config) - self.assertIsNotNone(saved_chat_config.chat_id) - self.assertEqual(saved_chat_config.external_id, chat_config_data.external_id) - self.assertEqual(saved_chat_config.language_iso_code, chat_config_data.language_iso_code) - self.assertEqual(saved_chat_config.language_name, chat_config_data.language_name) - self.assertEqual(saved_chat_config.title, chat_config_data.title) - self.assertEqual(saved_chat_config.is_private, chat_config_data.is_private) - self.assertEqual(saved_chat_config.reply_chance_percent, chat_config_data.reply_chance_percent) - self.assertEqual(saved_chat_config.release_notifications, chat_config_data.release_notifications) - self.assertEqual(saved_chat_config.media_mode, chat_config_data.media_mode) - self.assertEqual(saved_chat_config.chat_type, chat_config_data.chat_type) - - # Now, save should update the existing record - update_data = ChatConfigSave( - chat_id = saved_chat_config.chat_id, - language_iso_code = "fr", - language_name = "French", - title = "Chat Another", - is_private = False, - reply_chance_percent = 0, - release_notifications = ChatConfigDB.ReleaseNotifications.minor, - media_mode = ChatConfigDB.MediaMode.file, - chat_type = ChatConfigDB.ChatType.telegram, - ) - updated_chat_config = self.sql.chat_config_crud().save(update_data) - self.assertIsNotNone(updated_chat_config) - self.assertEqual(updated_chat_config.chat_id, saved_chat_config.chat_id) - self.assertEqual(updated_chat_config.language_iso_code, update_data.language_iso_code) - self.assertEqual(updated_chat_config.language_name, update_data.language_name) - self.assertEqual(updated_chat_config.title, update_data.title) - self.assertEqual(updated_chat_config.is_private, update_data.is_private) - self.assertEqual(updated_chat_config.reply_chance_percent, update_data.reply_chance_percent) - self.assertEqual(updated_chat_config.release_notifications, update_data.release_notifications) - self.assertEqual(updated_chat_config.media_mode, update_data.media_mode) - self.assertEqual(updated_chat_config.chat_type, update_data.chat_type) - - def test_delete_chat_config(self): - chat_config_data = ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram) - created_chat_config = self.sql.chat_config_crud().create(chat_config_data) - - deleted_chat_config = self.sql.chat_config_crud().delete(created_chat_config.chat_id) - - self.assertEqual(deleted_chat_config.chat_id, created_chat_config.chat_id) - self.assertIsNone(self.sql.chat_config_crud().get(created_chat_config.chat_id)) diff --git a/test/db/crud/test_chat_message.py b/test/db/crud/test_chat_message.py index 8deb6519..b2ec2a2c 100644 --- a/test/db/crud/test_chat_message.py +++ b/test/db/crud/test_chat_message.py @@ -6,9 +6,9 @@ from db.model.chat_config import ChatConfigDB from db.model.user import UserDB -from db.schema.chat_config import ChatConfigSave from db.schema.chat_message import ChatMessageSave from db.schema.user import UserSave +from features.chat.config.chat_config import ChatConfig class ChatMessageCRUDTest(unittest.TestCase): @@ -22,8 +22,8 @@ def tearDown(self): self.sql.end_session() def test_create_chat_message(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) user = self.sql.user_crud().create( UserSave( @@ -52,8 +52,8 @@ def test_create_chat_message(self): self.assertEqual(chat_message.text, chat_message_data.text) def test_get_chat_message(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) user = self.sql.user_crud().create( UserSave( @@ -84,11 +84,11 @@ def test_get_chat_message(self): self.assertEqual(fetched_chat_message.author_id, created_chat_message.author_id) def test_get_all_chat_messages(self): - chat1 = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat1 = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) - chat2 = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat2", chat_type = ChatConfigDB.ChatType.background), + chat2 = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat2", chat_type = ChatConfigDB.ChatType.background), ) user = self.sql.user_crud().create( UserSave( @@ -118,11 +118,11 @@ def test_get_all_chat_messages(self): self.assertEqual(fetched_chat_messages[i].author_id, chat_messages[i].author_id) def test_get_latest_chat_messages(self): - chat1 = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat1 = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) - chat2 = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat2", chat_type = ChatConfigDB.ChatType.background), + chat2 = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat2", chat_type = ChatConfigDB.ChatType.background), ) user = self.sql.user_crud().create( UserSave( @@ -188,8 +188,8 @@ def test_get_latest_chat_messages(self): self.assertEqual(fetched_chat2_messages[i].text, message.text) def test_update_chat_message(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) user = self.sql.user_crud().create( UserSave( @@ -224,8 +224,8 @@ def test_update_chat_message(self): self.assertEqual(updated_chat_message.text, update_data.text) def test_save_chat_message(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) user = self.sql.user_crud().create( UserSave( @@ -271,8 +271,8 @@ def test_save_chat_message(self): self.assertEqual(updated_chat_message.text, update_data.text) def test_delete_chat_message(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) user = self.sql.user_crud().create( UserSave( @@ -305,8 +305,8 @@ def test_delete_chat_message(self): ) def test_delete_older_than(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) user = self.sql.user_crud().create( UserSave( diff --git a/test/db/crud/test_chat_message_attachment.py b/test/db/crud/test_chat_message_attachment.py index 572b004c..9d646a29 100644 --- a/test/db/crud/test_chat_message_attachment.py +++ b/test/db/crud/test_chat_message_attachment.py @@ -5,9 +5,9 @@ from db.sql_util import SQLUtil from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfigSave from db.schema.chat_message import ChatMessage, ChatMessageSave from db.schema.chat_message_attachment import ChatMessageAttachment, ChatMessageAttachmentSave +from features.chat.config.chat_config import ChatConfig class ChatMessageAttachmentCRUDTest(unittest.TestCase): @@ -21,8 +21,8 @@ def tearDown(self): self.sql.end_session() def test_create_attachment(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) chat_message_db = self.sql.chat_message_crud().create( ChatMessageSave( @@ -57,8 +57,8 @@ def test_create_attachment(self): self.assertEqual(attachment.mime_type, attachment_data.mime_type) def test_create_attachment_auto_generates_id(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) chat_message_db = self.sql.chat_message_crud().create( ChatMessageSave( @@ -85,8 +85,8 @@ def test_create_attachment_auto_generates_id(self): self.assertEqual(attachment.size, 512) def test_create_with_external_id_only(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) chat_message_db = self.sql.chat_message_crud().create( ChatMessageSave( @@ -110,8 +110,8 @@ def test_create_with_external_id_only(self): self.assertEqual(attachment.message_id, chat_message.message_id) def test_get_attachment(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) chat_message_db = self.sql.chat_message_crud().create( ChatMessageSave( @@ -138,8 +138,8 @@ def test_get_attachment(self): self.assertEqual(fetched_attachment.message_id, created_attachment.message_id) def test_get_by_external_id(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) chat_message_db = self.sql.chat_message_crud().create( ChatMessageSave( @@ -174,8 +174,8 @@ def test_get_by_external_id_not_found(self): self.assertIsNone(fetched_attachment) def test_get_all_attachments(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) chat_message_db = self.sql.chat_message_crud().create( ChatMessageSave( @@ -214,8 +214,8 @@ def test_get_all_attachments(self): self.assertEqual(fetched_attachments[i].message_id, attachments[i].message_id) def test_get_by_message(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) chat_message_db = self.sql.chat_message_crud().create( ChatMessageSave( @@ -266,8 +266,8 @@ def test_get_by_message(self): self.assertEqual(len(non_existent_attachments), 0) def test_update_attachment(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) chat_message_db = self.sql.chat_message_crud().create( ChatMessageSave( @@ -311,8 +311,8 @@ def test_update_attachment(self): self.assertEqual(updated_attachment.mime_type, update_data.mime_type) def test_save_attachment(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) chat_message_db = self.sql.chat_message_crud().create( ChatMessageSave( @@ -368,8 +368,8 @@ def test_save_attachment(self): self.assertEqual(updated_attachment.mime_type, update_data.mime_type) def test_save_attachment_auto_generates_id(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) chat_message_db = self.sql.chat_message_crud().create( ChatMessageSave( @@ -396,8 +396,8 @@ def test_save_attachment_auto_generates_id(self): self.assertEqual(saved_attachment.size, 4096) def test_delete_attachment(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) chat_message_db = self.sql.chat_message_crud().create( ChatMessageSave( @@ -427,8 +427,8 @@ def test_delete_attachment(self): def test_integration_id_and_external_id_relationship(self): """Test the relationship between id and external_id fields""" - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) chat_message_db = self.sql.chat_message_crud().create( ChatMessageSave( @@ -467,8 +467,8 @@ def test_integration_id_and_external_id_relationship(self): self.assertEqual(by_id.external_id, "telegram_external_456") def test_delete_by_old_messages(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) old_message = self.sql.chat_message_crud().create( ChatMessageSave( diff --git a/test/db/crud/test_price_alert.py b/test/db/crud/test_price_alert.py index a196af4e..76acc3b8 100644 --- a/test/db/crud/test_price_alert.py +++ b/test/db/crud/test_price_alert.py @@ -5,9 +5,9 @@ from db.sql_util import SQLUtil from db.model.chat_config import ChatConfigDB -from db.schema.chat_config import ChatConfigSave from db.schema.price_alert import PriceAlertSave from db.schema.user import UserSave +from features.chat.config.chat_config import ChatConfig class PriceAlertCRUDTest(unittest.TestCase): @@ -33,8 +33,8 @@ def _create_test_user(self) -> UUID: return user_db.id def test_create_price_alert(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) user_id = self._create_test_user() price_alert_data = PriceAlertSave( @@ -57,8 +57,8 @@ def test_create_price_alert(self): self.assertEqual(price_alert.last_price_time, price_alert_data.last_price_time) def test_get_price_alert(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) user_id = self._create_test_user() price_alert_data = PriceAlertSave( @@ -83,11 +83,11 @@ def test_get_price_alert(self): self.assertEqual(fetched_price_alert.desired_currency, created_price_alert.desired_currency) def test_get_all_price_alerts(self): - chat1 = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat1 = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) - chat2 = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat2", chat_type = ChatConfigDB.ChatType.background), + chat2 = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat2", chat_type = ChatConfigDB.ChatType.background), ) user_id = self._create_test_user() price_alerts = [ @@ -116,11 +116,11 @@ def test_get_all_price_alerts(self): self.assertEqual(fetched_price_alerts[i].desired_currency, price_alerts[i].desired_currency) def test_get_chat_alerts(self): - chat1 = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat1 = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) - chat2 = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat2", chat_type = ChatConfigDB.ChatType.background), + chat2 = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat2", chat_type = ChatConfigDB.ChatType.background), ) user_id = self._create_test_user() @@ -157,8 +157,8 @@ def test_get_chat_alerts(self): self.assertEqual(fetched_chat1_alerts[i].desired_currency, chat1_alerts[i].desired_currency) def test_update_price_alert(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) user_id = self._create_test_user() price_alert_data = PriceAlertSave( @@ -191,8 +191,8 @@ def test_update_price_alert(self): self.assertEqual(updated_price_alert.last_price_time, update_data.last_price_time) def test_save_price_alert(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) user_id = self._create_test_user() price_alert_data = PriceAlertSave( @@ -235,8 +235,8 @@ def test_save_price_alert(self): self.assertEqual(updated_price_alert.last_price_time, update_data.last_price_time) def test_delete_price_alert(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) user_id = self._create_test_user() price_alert_data = PriceAlertSave( @@ -268,8 +268,8 @@ def test_delete_price_alert(self): ) def test_delete_stale(self): - chat = self.sql.chat_config_crud().create( - ChatConfigSave(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), + chat = self.sql.chat_config_repo().save( + ChatConfig(external_id = "chat1", chat_type = ChatConfigDB.ChatType.telegram), ) user_id = self._create_test_user() stale_alert = self.sql.price_alert_crud().create( diff --git a/test/db/sql_util.py b/test/db/sql_util.py index df081b92..519c68d8 100644 --- a/test/db/sql_util.py +++ b/test/db/sql_util.py @@ -1,6 +1,5 @@ from sqlalchemy.orm import Session -from db.crud.chat_config import ChatConfigCRUD from db.crud.chat_message import ChatMessageCRUD from db.crud.chat_message_attachment import ChatMessageAttachmentCRUD from db.crud.price_alert import PriceAlertCRUD @@ -43,11 +42,6 @@ def end_session(self): self.__session.close() self.__is_session_active = False - def chat_config_crud(self) -> ChatConfigCRUD: - if not self.__is_session_active: - self.start_session() - return ChatConfigCRUD(self.__session) - def chat_config_repo(self) -> ChatConfigRepository: if not self.__is_session_active: self.start_session() diff --git a/test/features/currencies/test_exchange_rate_fetcher.py b/test/features/currencies/test_exchange_rate_fetcher.py index ca721cfd..013c896d 100644 --- a/test/features/currencies/test_exchange_rate_fetcher.py +++ b/test/features/currencies/test_exchange_rate_fetcher.py @@ -7,7 +7,6 @@ from pydantic import SecretStr from requests_mock.mocker import Mocker -from db.crud.chat_config import ChatConfigCRUD from db.crud.tools_cache import ToolsCacheCRUD from db.crud.user import UserCRUD from db.model.user import UserDB @@ -28,7 +27,6 @@ class ExchangeRateFetcherTest(unittest.TestCase): cache_entry: ToolsCache mock_user_crud: UserCRUD mock_cache_crud: ToolsCacheCRUD - mock_chat_config_dao: ChatConfigCRUD mock_telegram_sdk: TelegramBotSDK def setUp(self): @@ -78,7 +76,6 @@ def setUp(self): self.mock_di.tools_cache_crud.get.return_value = None self.mock_sponsorship_dao = MagicMock() self.mock_sponsorship_dao.get_all_by_receiver.return_value = [] - self.mock_chat_config_dao = MagicMock() self.mock_telegram_sdk = MagicMock() # noinspection PyUnusedLocal From 68a91f1aa0708e9a898bf75f6bfff475901c08d1 Mon Sep 17 00:00:00 2001 From: Milos Marinkovic Date: Thu, 18 Jun 2026 15:35:50 +0200 Subject: [PATCH 6/7] Update OpenSpecs --- .../.openspec.yaml | 2 + .../design.md | 139 ++++++++++++++++++ .../proposal.md | 46 ++++++ .../specs/chat-config-persistence/spec.md | 101 +++++++++++++ .../tasks.md | 68 +++++++++ 5 files changed, 356 insertions(+) create mode 100644 openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/.openspec.yaml create mode 100644 openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/design.md create mode 100644 openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/proposal.md create mode 100644 openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/specs/chat-config-persistence/spec.md create mode 100644 openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/tasks.md diff --git a/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/.openspec.yaml b/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/.openspec.yaml new file mode 100644 index 00000000..c86b1d7f --- /dev/null +++ b/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-14 diff --git a/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/design.md b/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/design.md new file mode 100644 index 00000000..32387ce4 --- /dev/null +++ b/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/design.md @@ -0,0 +1,139 @@ +## Context + +`chat_configs` is one of the remaining persistence areas using the legacy `db/schema` + `db/crud` pattern. `ChatConfigCRUD` returns `ChatConfigDB` rows, and callers convert those rows with `ChatConfig.model_validate(...)`. That persistence shape leaks into `AuthorizationService`, `SettingsController`, Telegram/WhatsApp data resolvers, integration helpers, SDK code, announcements, and tests. + +Newer persistence areas in this codebase use a different boundary: + +``` +caller + │ + ▼ +repository ─────▶ SQLAlchemy DB model + │ ▲ + ▼ │ +domain dataclass ◀──── mapper +``` + +`UsageRecordRepository`, `PurchaseRecordRepository`, and `ChatMembershipRepository` are the local precedents. They keep SQLAlchemy models at the persistence edge and return feature-level domain dataclasses to callers. + +The most sensitive current behavior is in Telegram and WhatsApp chat resolution. Platform updates arrive as partial chat snapshots. When an existing chat is found, remote-owned fields refresh from the snapshot while DB-owned settings such as language, reply chance, release notifications, and media mode stay intact. `is_private` is treated as a platform fact and updates existing chats when the snapshot provides a non-null value. When no chat exists, the resolver persists a new chat from remote data with explicit mapper defaults, including release notifications based on privacy. + +## Goals / Non-Goals + +**Goals:** + +- Introduce a chat config domain dataclass, mapper, and repository that match the repository pattern used by newer persistence units. +- Keep `ChatConfigDB` as the only SQLAlchemy model for the existing `chat_configs` table. +- Preserve all current external API behavior and database schema behavior. +- Make creation defaults and remote-owned overrides explicit in code rather than relying on accidental DTO defaults. +- Add repository tests beside legacy CRUD tests, then migrate production callers through the new repository/domain boundary. +- Leave old CRUD/schema files in place until their remaining DB CRUD tests are intentionally retired. + +**Non-Goals:** + +- No database migration or schema change. +- No endpoint, payload, response, or OpenAPI contract change. +- No enum relocation in the first pass; `ChatConfigDB.ChatType`, `MediaMode`, and `ReleaseNotifications` remain where they are to avoid a whole-system enum migration. +- No immediate removal of `db/crud/chat_config.py` or `db/schema/chat_config.py`. + +## Decisions + +### 1. Keep One SQLAlchemy Model + +`ChatConfigDB` remains the only DB model and table definition for `chat_configs`. + +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 `ChatConfigDB`. + +**Rationale**: This matches `usage_record` and `purchase_record`: one DB model, one domain model, one mapper, one repository. A second DB model would create competing table definitions and migration risk without solving the persistence boundary problem. + +**Alternative considered**: Create a parallel SQLAlchemy model. Rejected because it duplicates table ownership and makes Alembic behavior harder to reason about. + +### 2. Use a Nullable Domain ID for Create Parity + +The chat config domain dataclass may carry `chat_id: UUID | None = None` during create flows. The repository lets SQLAlchemy generate `chat_id` using the existing `ChatConfigDB.chat_id` default when no ID is present, then returns the persisted domain object with a concrete ID. + +**Rationale**: `chat_configs` currently generates IDs in the DB model. Preserving that avoids changing identity ownership during this refactor. It also keeps the first milestone close to existing `ChatConfigSave` behavior while moving persistence behind a repository. + +**Alternative considered**: Require callers to generate UUIDs before saving. Rejected for the first pass because it changes current object lifecycle semantics without a clear benefit. + +### 3. Repository Saves Full Domain Objects and Remote Snapshots + +The repository provides persistence operations: + +- `get(chat_id) -> ChatConfig | None` +- `get_all(skip, limit) -> list[ChatConfig]` +- `get_by_external_identifiers(external_id, chat_type) -> ChatConfig | None` +- `save(chat_config: ChatConfig | ChatConfigRemoteData) -> ChatConfig` +- `delete(chat_id) -> ChatConfig | None` + +`save(ChatConfig)` treats the domain object as a complete persisted object. If `chat_id` points at an existing row, the row is updated with all domain fields. If `chat_id` is absent or no row exists for it, a new row is inserted and SQLAlchemy generates the ID when needed. + +`save(ChatConfigRemoteData)` treats the input as a remote/platform snapshot. The repository looks up by `(external_id, chat_type)`. If a row exists, mapper-owned merge logic applies only remote-owned updates. If no row exists, mapper-owned creation logic builds a full `ChatConfig` from the snapshot plus explicit defaults. + +**Rationale**: The repository stays responsible for persistence orchestration while the mapper owns cross-shape conversion. Callers can save full domain objects when they own all fields, or save remote data when they only have a platform snapshot. + +**Alternative considered**: Keep separate `create`, `update`, and `get_by_external_identifiers_or_create` methods. Rejected because it forced platform callers to duplicate the existing-vs-missing branch and made the new repo less aligned with the current `save` usage. + +### 4. Resolve Platform Snapshots with Explicit Ownership + +Telegram and WhatsApp chat config resolution should follow this shape after their migration milestone: + +``` +remote_data = ChatConfigRemoteData( + external_id = snapshot.external_id, + chat_type = snapshot.chat_type, + title = snapshot.title, + is_private = snapshot.is_private, + language_iso_code = snapshot.language_iso_code, +) + +return repo.save(remote_data) +``` + +For existing chats, `language_iso_code`, `language_name`, `reply_chance_percent`, `release_notifications`, and `media_mode` remain DB-owned. `title` and non-null `is_private` refresh from remote data. For new chats, `from_remote_data` uses explicit defaults: private defaults to `True`, release notifications are `major` for private chats and `none` for public chats, and media mode defaults to `photo`. + +**Rationale**: The resolver stops mutating the incoming mapped object and instead sends a typed remote snapshot to the repository. The mapper makes field ownership clear in one conversion boundary. + +**Alternative considered**: Keep mutating a `ChatConfigSave`-like object before saving. Rejected because the object still conflates partial platform snapshots with full persistence data. + +### 5. Migrate Callers Through Domain Boundaries + +The new repository is added to DI immediately but remains inert until a caller uses it. The original migration plan used small review gates: + +1. Add domain/mapper/repo/DI and tests only. +2. Migrate `SettingsController` direct chat config reads/writes. +3. Migrate Telegram and WhatsApp chat config resolution. +4. Migrate lower-risk read paths such as announcements, SDK lookup code, and integration helpers. +5. Migrate `AuthorizationService` after the lower-risk paths have been reviewed. +6. Remove legacy DI access after production imports are gone. +7. Remove legacy CRUD/schema files only when their remaining tests are retired. + +During implementation, review feedback broadened the migration to all domain-layer callers including authorization. The production boundary now uses `chat_config_repo` and the new domain `ChatConfig` across DI, settings, authorization, membership, platform resolvers, integrations, announcements, SDK lookup code, responders, and prompt-resolver paths. + +**Rationale**: Once settings and repository behavior were reviewed, keeping mixed legacy/domain model types in the domain layer became more risky than migrating the rest of the domain boundary together. Removing `chat_config_crud` from DI prevents new production code from drifting back to the legacy path. + +**Alternative considered**: Big-bang replace every `db.schema.chat_config` and `chat_config_crud` import. Rejected due to broad blast radius and poor reviewability. + +## Risks / Trade-offs + +- **Repository and CRUD temporarily coexist** -> Mitigation: keep both test suites green and remove CRUD only after `rg "chat_config_crud|db.schema.chat_config"` shows no production imports. +- **Partial migration causes mixed model types in callers** -> Mitigation: migrate one owner boundary at a time and adjust mocks/tests per milestone. +- **Existing resolver behavior changes accidentally** -> Mitigation: use Telegram/WhatsApp data resolver tests as canaries, especially existing-chat remote-field updates, DB-owned field preservation, and new-chat default tests. +- **Enum migration expands scope** -> Mitigation: do not move `ChatConfigDB` nested enums in this change. +- **Authorization regression** -> Mitigation: migrate `AuthorizationService` only after repository behavior is covered, then run authorization/settings tests and the full offline suite. + +## Migration Plan + +1. Add the new domain model, mapper, repository, DI property, and SQL test helper. +2. Add mapper and repository tests that mirror the existing CRUD behavior. +3. Migrate production callers through API/service boundaries, platform resolvers, integrations, announcements, SDK lookup code, responders, and authorization. +4. After each milestone, run the focused tests for the migrated boundary. +5. When all production callers use the repository, remove legacy DI access to `chat_config_crud`. +6. Remove legacy chat config CRUD/schema and their now-obsolete tests in a final cleanup after review. +7. 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. `is_private` is intentionally treated as a remote-owned platform fact when the snapshot provides it. diff --git a/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/proposal.md b/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/proposal.md new file mode 100644 index 00000000..57235481 --- /dev/null +++ b/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/proposal.md @@ -0,0 +1,46 @@ +## Why + +Chat config persistence still uses the legacy `db/schema` + `db/crud` pattern, which leaks SQLAlchemy/Pydantic persistence types into API controllers, authorization, and platform data resolvers. Newer persistence areas such as usage records and chat memberships use feature-level domain dataclasses, explicit DB/domain mappers, and repositories, making persistence behavior easier to test and refactor safely. + +## What Changes + +- Add a feature-level chat config domain model, mapper, and repository beside the existing `ChatConfigDB` SQLAlchemy model. +- Keep `ChatConfigDB` as the single database model and preserve the existing `chat_configs` table, columns, indexes, defaults, and uniqueness behavior. +- Add the new repository to DI immediately, then remove DI access to the legacy CRUD after production callers are migrated. +- Preserve current chat config behavior while migrating callers gradually: + - direct domain saves persist the full chat config object; + - remote/platform snapshot saves update only remote-owned fields for existing chats; + - newly discovered remote/platform chats receive explicit defaults in the mapper. +- Migrate production callers from API/service boundaries through platform resolvers, integrations, announcements, SDK lookup code, and authorization. +- Keep legacy CRUD tests until the CRUD file is no longer used by production code; add repository/mapper tests first and maintain both test sets during the transition. + +## Capabilities + +### New Capabilities + +- `chat-config-persistence`: Domain-model and repository behavior for creating, reading, updating, and resolving chat configurations without exposing SQLAlchemy DB models or legacy Pydantic persistence schemas to callers. + +### Modified Capabilities + +_(None — no existing chat config persistence spec.)_ + +## Impact + +**Code** +- New feature-level chat config files under `src/features/chat/config/` or an equivalent feature-local package. +- `src/di/di.py` gains `chat_config_repo`; `chat_config_crud` is removed from DI once production callers no longer use it. +- `test/db/sql_util.py` gains a `chat_config_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, index, enum, or migration changes are intended. +- `src/db/model/chat_config.py` remains the only SQLAlchemy representation for `chat_configs`. + +**API** +- No external API route, payload, response, or OpenAPI behavior changes are intended. +- Settings endpoints should continue returning the same externally visible data while their internal persistence dependency changes. + +**Tests** +- New mapper tests verify DB/domain round trips, remote snapshot conversion, merge behavior, enum fields, generated-ID create behavior, and `None` handling. +- New repository tests mirror existing `test/db/crud/test_chat_config.py` behavior and cover `ChatConfigRemoteData` save semantics. +- Existing settings, authorization, Telegram, WhatsApp, integration, announcement, SDK, and responder tests remain behavior canaries through migration. diff --git a/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/specs/chat-config-persistence/spec.md b/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/specs/chat-config-persistence/spec.md new file mode 100644 index 00000000..3fab3db4 --- /dev/null +++ b/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/specs/chat-config-persistence/spec.md @@ -0,0 +1,101 @@ +## ADDED Requirements + +### Requirement: Chat config repository returns domain models +The system SHALL provide a chat config repository that persists through the existing `ChatConfigDB` table model while accepting and returning feature-level chat config domain dataclasses. + +#### Scenario: Fetch by chat ID returns domain model +- **WHEN** a chat config exists for a chat ID +- **THEN** the chat config repository returns a chat config domain dataclass with the persisted field values + +#### Scenario: Missing chat ID returns none +- **WHEN** no chat config exists for a chat ID +- **THEN** the chat config repository returns `None` + +#### Scenario: Save inserts pure chat config +- **WHEN** the chat config repository saves a domain dataclass without a persisted `chat_id` +- **THEN** the repository inserts a `chat_configs` row using the existing database model defaults +- **THEN** the repository returns a domain dataclass with a concrete persisted `chat_id` + +#### Scenario: Save updates pure chat config +- **WHEN** the chat config repository saves a domain dataclass with an existing `chat_id` +- **THEN** the repository updates that row +- **THEN** subsequent reads return the updated domain field values + +### Requirement: External identifier lookup is preserved +The system SHALL preserve current chat config lookup behavior by `(external_id, chat_type)`. + +#### Scenario: Existing external identifier returns domain model +- **WHEN** a chat config exists with a matching `external_id` and `chat_type` +- **THEN** the chat config repository returns that chat config as a domain dataclass + +#### Scenario: Missing external identifier returns none +- **WHEN** no chat config exists with a matching `external_id` and `chat_type` +- **THEN** the chat config repository returns `None` + +#### Scenario: External identifiers remain unique by chat type +- **WHEN** a chat config is created or updated +- **THEN** the existing unique database constraint for non-null `(external_id, chat_type)` remains the persistence constraint + +### Requirement: Remote chat snapshots use explicit conversion +The system SHALL provide a non-null remote snapshot save flow for platform chat resolution that applies remote-owned updates when an existing chat config is found and creates from explicit defaults when no chat config is found. + +#### Scenario: Existing chat merges remote-owned fields +- **WHEN** `save(ChatConfigRemoteData)` finds an existing chat config by `(external_id, chat_type)` +- **THEN** it updates remote-owned fields from non-null snapshot values +- **THEN** it preserves DB-owned fields that are not part of the remote update set + +#### Scenario: Missing chat creates from remote data +- **WHEN** `save(ChatConfigRemoteData)` does not find an existing chat config by `(external_id, chat_type)` +- **THEN** it creates a new chat config from the remote snapshot and mapper defaults +- **THEN** it returns the persisted domain dataclass + +#### Scenario: Missing privacy defaults to private +- **WHEN** `save(ChatConfigRemoteData)` creates a new chat config and the snapshot privacy value is `None` +- **THEN** the new chat config defaults `is_private` to `True` +- **THEN** release notifications default according to the resolved privacy value + +### Requirement: Platform chat resolution preserves existing behavior +The system SHALL preserve current Telegram and WhatsApp chat config resolution behavior while moving persistence to the repository. + +#### Scenario: Existing Telegram chat preserves DB-owned settings +- **WHEN** a Telegram update resolves a chat config that already exists +- **THEN** the persisted chat config keeps its existing language, reply chance, release notifications, and media mode +- **THEN** remote-owned fields such as title and non-null `is_private` refresh from the platform snapshot + +#### Scenario: New Telegram chat uses explicit defaults +- **WHEN** a Telegram update resolves a chat config that does not exist +- **THEN** the new persisted chat config uses explicit defaults matching current Telegram resolver behavior + +#### Scenario: Existing WhatsApp chat preserves DB-owned settings +- **WHEN** a WhatsApp update resolves a chat config that already exists +- **THEN** the persisted chat config keeps its existing language, reply chance, release notifications, and media mode +- **THEN** remote-owned fields such as title and non-null `is_private` refresh from the platform snapshot + +#### Scenario: New WhatsApp chat uses explicit defaults +- **WHEN** a WhatsApp update resolves a chat config that does not exist +- **THEN** the new persisted chat config uses explicit defaults matching current WhatsApp resolver behavior + +### Requirement: Repository migration preserves production behavior +The system SHALL migrate production chat config callers from legacy chat config CRUD/schema usage to the repository without changing external API behavior. + +#### Scenario: Repository is added before production migration +- **WHEN** the chat config repository is added to DI +- **THEN** existing legacy CRUD callers continue to work until they are explicitly migrated + +#### Scenario: Production callers use repository domain models +- **WHEN** production chat config callers are migrated +- **THEN** API controllers, authorization, domain services, platform resolvers, integrations, announcements, SDK lookup code, and responders use `chat_config_repo` and the feature-level chat config domain model +- **THEN** they do not import `db.schema.chat_config` for production chat config behavior + +#### Scenario: Settings controller migration preserves API behavior +- **WHEN** settings controller chat config persistence is migrated to the repository +- **THEN** settings API routes, payloads, responses, validation behavior, and sorting remain externally unchanged + +#### Scenario: DI exposes only repository access for production chat config persistence +- **WHEN** production callers no longer use legacy chat config CRUD +- **THEN** DI exposes `chat_config_repo` for chat config persistence +- **THEN** DI no longer exposes `chat_config_crud` + +#### Scenario: Legacy CRUD removed only after production imports are gone +- **WHEN** no production code imports `chat_config_crud` or `db.schema.chat_config` +- **THEN** the legacy CRUD/schema files and their obsolete tests may be removed diff --git a/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/tasks.md b/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/tasks.md new file mode 100644 index 00000000..ab399e1d --- /dev/null +++ b/openspec/changes/archive/2026-06-18-migrate-chat-config-to-repository/tasks.md @@ -0,0 +1,68 @@ +## 1. Repository Foundation + +- [x] 1.1 Create the feature-level chat config package and domain dataclass while leaving `ChatConfigDB` as the only SQLAlchemy model. +- [x] 1.2 Create `chat_config_mapper.py` with DB-to-domain and domain-to-DB conversion, including full field coverage and `None` handling. +- [x] 1.3 Create `ChatConfigRepository` with `get`, `get_all`, `get_by_external_identifiers`, `save(ChatConfig | ChatConfigRemoteData)`, and `delete`. +- [x] 1.4 Add `ChatConfigRemoteData` and mapper-owned remote conversion/merge behavior for snapshot saves. +- [x] 1.5 Wire `chat_config_repo` into `src/di/di.py` without removing or changing `chat_config_crud`. +- [x] 1.6 Add `chat_config_repo()` to `test/db/sql_util.py`. +- [x] 1.7 Add mapper tests covering DB/domain round trips, enum fields, remote snapshot conversion, remote merge behavior, and `None` handling. +- [x] 1.8 Add repository tests mirroring existing `test/db/crud/test_chat_config.py` behavior plus `ChatConfigRemoteData` save behavior. +- [x] 1.9 Run focused repository and legacy CRUD tests. +- [x] 1.10 Stop for manual review of the new repository shape before migrating production callers. + +## 2. Settings Controller Migration + +- [x] 2.1 Replace direct chat config CRUD reads in `SettingsController.fetch_all_chat_settings` with `chat_config_repo`. +- [x] 2.2 Replace direct chat config CRUD writes in `SettingsController.__apply_chat_config_changes` with `chat_config_repo`. +- [x] 2.3 Update settings-controller mocks and fixtures to use the new chat config domain model for migrated paths. +- [x] 2.4 Verify settings API responses, validation errors, authorization checks, and chat sorting remain unchanged. +- [x] 2.5 Run focused settings tests. +- [x] 2.6 Stop for manual review of the settings-controller migration. + +## 3. Platform Resolver Migration + +- [x] 3.1 Migrate Telegram chat config resolution to build `ChatConfigRemoteData` and call `chat_config_repo.save(remote_data)`. +- [x] 3.2 Replace Telegram resolver mutation of incoming mapped data with explicit remote snapshot persistence. +- [x] 3.3 Preserve existing Telegram behavior for DB-owned fields: language, reply chance, release notifications, and media mode; update non-null remote-owned `is_private`. +- [x] 3.4 Migrate WhatsApp chat config resolution with the same remote snapshot repository pattern. +- [x] 3.5 Preserve existing WhatsApp behavior for DB-owned fields: language, reply chance, release notifications, and media mode; update non-null remote-owned `is_private`. +- [x] 3.6 Update Telegram and WhatsApp resolver tests to assert existing-chat preservation, new-chat defaults, and no mutation dependency on legacy `ChatConfigSave` persistence behavior. +- [x] 3.7 Run focused Telegram and WhatsApp resolver tests. +- [x] 3.8 Stop for manual review of platform resolver behavior. + +## 4. Remaining Production Caller Migration + +- [x] 4.1 Migrate lower-risk chat config lookup paths in integrations, announcements, SDKs, responders, and support code from legacy CRUD/schema use to the repository/domain model. +- [x] 4.2 Update affected unit tests and mocks for migrated lower-risk callers. +- [x] 4.3 Run focused tests for migrated integration, announcement, SDK, and responder paths. +- [x] 4.4 Stop for manual review before migrating authorization. + +## 5. Authorization Migration + +- [x] 5.1 Migrate `AuthorizationService.validate_chat` to use `chat_config_repo` while preserving malformed-ID and not-found error behavior. +- [x] 5.2 Migrate `AuthorizationService.get_authorized_chats` to consume repository domain models while preserving admin-discovery behavior and sort order. +- [x] 5.3 Update DI `invoker_chat` typing and related tests for the new chat config domain model. +- [x] 5.4 Update authorization tests for migrated repository usage. +- [x] 5.5 Run focused authorization and settings tests. +- [x] 5.6 Stop for manual review of authorization behavior. + +## 6. Legacy Cleanup + +- [x] 6.1 Search production code for `chat_config_crud`, `db.schema.chat_config`, `ChatConfig.model_validate`, and `ChatConfigSave`. +- [x] 6.2 Remove legacy `chat_config_crud` access from DI after production callers are migrated. +- [x] 6.3 Confirm production references to legacy chat config CRUD/schema remain only in the legacy CRUD/schema modules themselves. +- [x] 6.4 Replace remaining test-only `chat_config_crud` / `ChatConfigSave` fixture setup in DB CRUD tests for chat messages, chat message attachments, and price alerts. +- [x] 6.5 Remove obsolete legacy chat config CRUD tests after repository coverage is accepted as the replacement. +- [x] 6.6 Remove legacy `db/crud/chat_config.py`, `db/schema/chat_config.py`, and `SQLUtil.chat_config_crud()` after no production or test references remain. +- [x] 6.7 Update migrated API/feature test fixtures to use the new domain model and repository helpers. +- [x] 6.8 Run the broad chat/settings/authorization/platform test set. +- [x] 6.9 Stop for manual review before final verification. + +## 7. Final Verification + +- [x] 7.1 Run `pipenv run pytest`. +- [x] 7.2 Run `pipenv run pre-commit run --all-files --show-diff-on-failure`. +- [x] 7.3 Confirm no database migration was generated or required for this change. +- [x] 7.4 Confirm no external API/OpenAPI behavior changed. +- [x] 7.5 Summarize remaining risks and completion status for final review. From 6e5b6fee4654ffac00f1fe959a6c93fc9e9a267c Mon Sep 17 00:00:00 2001 From: Milos Marinkovic Date: Thu, 18 Jun 2026 15:36:26 +0200 Subject: [PATCH 7/7] Bump version --- docs/open-api-docs.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/open-api-docs.yaml b/docs/open-api-docs.yaml index cd12d948..1dad1463 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.0 + version: 5.19.1 license: name: MIT url: https://opensource.org/licenses/MIT diff --git a/pyproject.toml b/pyproject.toml index 4e30741c..a206b2b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "the-agent" -version = "5.19.0" +version = "5.19.1" [tool.setuptools] package-dir = {"" = "src"}