From a3ceef9040b45bfb07cfe2f54af18f31cad4e97a Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 6 Aug 2025 18:43:27 +0300 Subject: [PATCH 01/25] Add google libs --- backend/poetry.lock | 444 +++++++++++++++++++++++++++-------------- backend/pyproject.toml | 3 + 2 files changed, 299 insertions(+), 148 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index 83863b1fa7..6496874cfd 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -6,7 +6,6 @@ version = "2.6.1" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, @@ -18,7 +17,6 @@ version = "3.12.15" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc"}, {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af"}, @@ -118,7 +116,7 @@ propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""] +speedups = ["Brotli", "aiodns (>=3.3.0)", "brotlicffi"] [[package]] name = "aiosignal" @@ -126,7 +124,6 @@ version = "1.4.0" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, @@ -141,7 +138,6 @@ version = "4.24.0" description = "A fully-featured and blazing-fast Python API client to interact with Algolia." optional = false python-versions = ">=3.8.1" -groups = ["main"] files = [ {file = "algoliasearch-4.24.0-py3-none-any.whl", hash = "sha256:223d188c2385895d6d49779ad3cd79e549963629141087cc8a38a9a7212b6ccf"}, {file = "algoliasearch-4.24.0.tar.gz", hash = "sha256:82854b633bc604e1b84073dfaa773d62da868dcf361e019ac87a294fc1dd4746"}, @@ -161,7 +157,6 @@ version = "4.0.0" description = "Algolia Search integration for Django" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "algoliasearch_django-4.0.0-py2.py3-none-any.whl", hash = "sha256:d160b86cd999607e9b3b0773a712e196e251af2b7dcb2480e40ef09440f3c80a"}, {file = "algoliasearch_django-4.0.0.tar.gz", hash = "sha256:c0acb8231163c16757d9e4c37a0ce882b89c4640a6dc836daaf479fd73c427b5"}, @@ -176,7 +171,6 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -188,7 +182,6 @@ version = "4.10.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, @@ -207,7 +200,6 @@ version = "3.9.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c"}, {file = "asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142"}, @@ -222,7 +214,6 @@ version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -234,19 +225,18 @@ version = "25.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [package.extras] -benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "boto3" @@ -254,7 +244,6 @@ version = "1.40.2" description = "The AWS SDK for Python" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "boto3-1.40.2-py3-none-any.whl", hash = "sha256:3d99325ee874190e8f3bfd38823987327c826cdfbab943420851bdb7684d727c"}, {file = "boto3-1.40.2.tar.gz", hash = "sha256:2dfbc214fdbf94abfd61eec687ea39089d05af43bb00be792c76f3a6c1393f7b"}, @@ -274,7 +263,6 @@ version = "1.40.2" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "botocore-1.40.2-py3-none-any.whl", hash = "sha256:a31e6269af05498f8dc1c7f2b3f34448a0f16c79a8601c0389ecddab51b2c2ab"}, {file = "botocore-1.40.2.tar.gz", hash = "sha256:77c4710bf37b28e897833b5b1f47d6a83e45a29985cd01a560dfdb8b6ad524e5"}, @@ -288,13 +276,23 @@ urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version > [package.extras] crt = ["awscrt (==0.23.8)"] +[[package]] +name = "cachetools" +version = "5.5.2" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, + {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, +] + [[package]] name = "certifi" version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, @@ -306,7 +304,6 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -386,7 +383,6 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -398,7 +394,6 @@ version = "3.4.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, @@ -500,7 +495,6 @@ version = "8.2.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" -groups = ["dev"] files = [ {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, @@ -515,12 +509,10 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "dev", "test"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""} [[package]] name = "coverage" @@ -528,7 +520,6 @@ version = "7.10.2" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" -groups = ["test"] files = [ {file = "coverage-7.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65"}, {file = "coverage-7.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8"}, @@ -621,7 +612,7 @@ files = [ ] [package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +toml = ["tomli"] [[package]] name = "cryptography" @@ -629,7 +620,6 @@ version = "45.0.5" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" -groups = ["main"] files = [ {file = "cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8"}, {file = "cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d"}, @@ -674,10 +664,10 @@ files = [ cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs ; python_full_version >= \"3.8.0\"", "sphinx-rtd-theme (>=3.0.0) ; python_full_version >= \"3.8.0\""] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8.0\""] -pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==45.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] @@ -689,7 +679,6 @@ version = "1.15.4" description = "CSS unobfuscator and beautifier." optional = false python-versions = "*" -groups = ["dev"] files = [ {file = "cssbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:78c84d5e5378df7d08622bbd0477a1abdbd209680e95480bf22f12d5701efc98"}, {file = "cssbeautifier-1.15.4.tar.gz", hash = "sha256:9bb08dc3f64c101a01677f128acf01905914cf406baf87434dcde05b74c0acf5"}, @@ -706,7 +695,6 @@ version = "0.6.7" description = "Easily serialize dataclasses to and from JSON." optional = false python-versions = "<4.0,>=3.7" -groups = ["main"] files = [ {file = "dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a"}, {file = "dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0"}, @@ -722,7 +710,6 @@ version = "0.4.0" description = "Distribution utilities" optional = false python-versions = "*" -groups = ["dev"] files = [ {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, @@ -734,7 +721,6 @@ version = "1.9.0" description = "Distro - an OS platform information API" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, @@ -746,7 +732,6 @@ version = "5.2.4" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" -groups = ["main"] files = [ {file = "django-5.2.4-py3-none-any.whl", hash = "sha256:60c35bd96201b10c6e7a78121bd0da51084733efa303cc19ead021ab179cef5e"}, {file = "django-5.2.4.tar.gz", hash = "sha256:a1228c384f8fa13eebc015196db7b3e08722c5058d4758d20cb287503a540d8f"}, @@ -767,7 +752,6 @@ version = "2.5.1" description = "A helper for organizing Django settings." optional = false python-versions = "<4.0,>=3.8" -groups = ["main"] files = [ {file = "django-configurations-2.5.1.tar.gz", hash = "sha256:6e5083757e2bbdf9bb7850567536b96a93515f6b17503d74928ff628db2e0e94"}, {file = "django_configurations-2.5.1-py3-none-any.whl", hash = "sha256:ceb84858da2dac846b15e715c2fd936cfc4c7917c074aff8d31700564093955e"}, @@ -789,7 +773,6 @@ version = "4.7.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "django_cors_headers-4.7.0-py3-none-any.whl", hash = "sha256:f1c125dcd58479fe7a67fe2499c16ee38b81b397463cf025f0e2c42937421070"}, {file = "django_cors_headers-4.7.0.tar.gz", hash = "sha256:6fdf31bf9c6d6448ba09ef57157db2268d515d94fc5c89a0a1028e1fc03ee52b"}, @@ -805,7 +788,6 @@ version = "1.4.3" description = "Django Ninja - Fast Django REST framework" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "django_ninja-1.4.3-py3-none-any.whl", hash = "sha256:f3204137a059437b95677049474220611f1cf9efedba9213556474b75168fa01"}, {file = "django_ninja-1.4.3.tar.gz", hash = "sha256:e46d477ca60c228d2a5eb3cc912094928ea830d364501f966661eeada67cb038"}, @@ -826,7 +808,6 @@ version = "5.4.0" description = "Full featured redis cache backend for Django." optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "django-redis-5.4.0.tar.gz", hash = "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42"}, {file = "django_redis-5.4.0-py3-none-any.whl", hash = "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b"}, @@ -845,7 +826,6 @@ version = "1.14.6" description = "Support for many storage backends in Django" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "django_storages-1.14.6-py3-none-any.whl", hash = "sha256:11b7b6200e1cb5ffcd9962bd3673a39c7d6a6109e8096f0e03d46fab3d3aabd9"}, {file = "django_storages-1.14.6.tar.gz", hash = "sha256:7a25ce8f4214f69ac9c7ce87e2603887f7ae99326c316bc8d2d75375e09341c9"}, @@ -870,7 +850,6 @@ version = "1.36.4" description = "HTML Template Linter and Formatter" optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "djlint-1.36.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2dfb60883ceb92465201bfd392291a7597c6752baede6fbb6f1980cac8d6c5c"}, {file = "djlint-1.36.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc6a1320c0030244b530ac200642f883d3daa451a115920ef3d56d08b644292"}, @@ -913,7 +892,6 @@ version = "0.17.1" description = "EditorConfig File Locator and Interpreter for Python" optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "editorconfig-0.17.1-py3-none-any.whl", hash = "sha256:1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82"}, {file = "editorconfig-0.17.1.tar.gz", hash = "sha256:23c08b00e8e08cc3adcddb825251c497478df1dada6aefeb01e626ad37303745"}, @@ -928,7 +906,6 @@ version = "2.14.1" description = "Emoji for Python" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "emoji-2.14.1-py3-none-any.whl", hash = "sha256:35a8a486c1460addb1499e3bf7929d3889b2e2841a57401903699fef595e942b"}, {file = "emoji-2.14.1.tar.gz", hash = "sha256:f8c50043d79a2c1410ebfae833ae1868d5941a67a6cd4d18377e2eb0bd79346b"}, @@ -943,7 +920,6 @@ version = "2.1.1" description = "execnet: rapid multi-Python deployment" optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, @@ -958,7 +934,6 @@ version = "3.18.0" description = "A platform independent file lock." optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, @@ -967,7 +942,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] -typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] +typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "frozenlist" @@ -975,7 +950,6 @@ version = "1.7.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a"}, {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61"}, @@ -1089,7 +1063,6 @@ version = "2.0" description = "The geodesic routines from GeographicLib" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "geographiclib-2.0-py3-none-any.whl", hash = "sha256:6b7225248e45ff7edcee32becc4e0a1504c606ac5ee163a5656d482e0cd38734"}, {file = "geographiclib-2.0.tar.gz", hash = "sha256:f7f41c85dc3e1c2d3d935ec86660dc3b2c848c83e17f9a9e51ba9d5146a15859"}, @@ -1101,7 +1074,6 @@ version = "2.4.1" description = "Python Geocoding Toolbox" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "geopy-2.4.1-py3-none-any.whl", hash = "sha256:ae8b4bc5c1131820f4d75fce9d4aaaca0c85189b3aa5d64c3dcaf5e3b7b882a7"}, {file = "geopy-2.4.1.tar.gz", hash = "sha256:50283d8e7ad07d89be5cb027338c6365a32044df3ae2556ad3f52f4840b3d0d1"}, @@ -1119,13 +1091,130 @@ dev-test = ["coverage", "pytest (>=3.10)", "pytest-asyncio (>=0.17)", "sphinx (< requests = ["requests (>=2.16.2)", "urllib3 (>=1.24.2)"] timezone = ["pytz"] +[[package]] +name = "google-api-core" +version = "2.25.1" +description = "Google API client core library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7"}, + {file = "google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8"}, +] + +[package.dependencies] +google-auth = ">=2.14.1,<3.0.0" +googleapis-common-protos = ">=1.56.2,<2.0.0" +proto-plus = {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""} +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" +requests = ">=2.18.0,<3.0.0" + +[package.extras] +async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.0)"] +grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0)", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0)"] +grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] +grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] + +[[package]] +name = "google-api-python-client" +version = "2.178.0" +description = "Google API Client Library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google_api_python_client-2.178.0-py3-none-any.whl", hash = "sha256:f420adcd050150ff1baefa817e96e1ffa16872744f53471cd34096612e580c34"}, + {file = "google_api_python_client-2.178.0.tar.gz", hash = "sha256:99cba921eb471bb5973b780c653ac54d96eef8a42f1b7375b7ab98f257a4414c"}, +] + +[package.dependencies] +google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0" +google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" +google-auth-httplib2 = ">=0.2.0,<1.0.0" +httplib2 = ">=0.19.0,<1.0.0" +uritemplate = ">=3.0.1,<5" + +[[package]] +name = "google-auth" +version = "2.40.3" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca"}, + {file = "google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] +enterprise-cert = ["cryptography", "pyopenssl"] +pyjwt = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0)"] +testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0)", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] +urllib3 = ["packaging", "urllib3"] + +[[package]] +name = "google-auth-httplib2" +version = "0.2.0" +description = "Google Authentication Library: httplib2 transport" +optional = false +python-versions = "*" +files = [ + {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, + {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, +] + +[package.dependencies] +google-auth = "*" +httplib2 = ">=0.19.0" + +[[package]] +name = "google-auth-oauthlib" +version = "1.2.2" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "google_auth_oauthlib-1.2.2-py3-none-any.whl", hash = "sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2"}, + {file = "google_auth_oauthlib-1.2.2.tar.gz", hash = "sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684"}, +] + +[package.dependencies] +google-auth = ">=2.15.0" +requests-oauthlib = ">=0.7.0" + +[package.extras] +tool = ["click (>=6.0.0)"] + +[[package]] +name = "googleapis-common-protos" +version = "1.70.0" +description = "Common protobufs used in Google APIs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"}, + {file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"}, +] + +[package.dependencies] +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] + [[package]] name = "graphql-core" version = "3.2.6" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." optional = false python-versions = "<4,>=3.6" -groups = ["main"] files = [ {file = "graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f"}, {file = "graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab"}, @@ -1137,8 +1226,6 @@ version = "3.2.3" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "python_version == \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" files = [ {file = "greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be"}, {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac"}, @@ -1206,7 +1293,6 @@ version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, @@ -1228,7 +1314,6 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -1240,7 +1325,6 @@ version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -1256,13 +1340,26 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] trio = ["trio (>=0.22.0,<1.0)"] +[[package]] +name = "httplib2" +version = "0.22.0" +description = "A comprehensive HTTP client library." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, + {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, +] + +[package.dependencies] +pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} + [[package]] name = "httpx" version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -1275,7 +1372,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1287,7 +1384,6 @@ version = "0.4.1" description = "Consume Server-Sent Event (SSE) messages with HTTPX." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37"}, {file = "httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e"}, @@ -1299,7 +1395,6 @@ version = "4.12.3" description = "Python humanize utilities" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "humanize-4.12.3-py3-none-any.whl", hash = "sha256:2cbf6370af06568fa6d2da77c86edb7886f3160ecd19ee1ffef07979efc597f6"}, {file = "humanize-4.12.3.tar.gz", hash = "sha256:8430be3a615106fdfceb0b2c1b41c4c98c6b0fc5cc59663a5539b111dd325fb0"}, @@ -1314,7 +1409,6 @@ version = "2.6.12" description = "File identification library for Python" optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2"}, {file = "identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6"}, @@ -1329,7 +1423,6 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -1344,7 +1437,6 @@ version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -1356,7 +1448,6 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -1374,7 +1465,6 @@ version = "0.10.0" description = "Fast iterable JSON parser." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303"}, {file = "jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e"}, @@ -1461,7 +1551,6 @@ version = "1.0.1" description = "JSON Matching Expressions" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, @@ -1473,7 +1562,6 @@ version = "1.15.4" description = "JavaScript unobfuscator and beautifier." optional = false python-versions = "*" -groups = ["dev"] files = [ {file = "jsbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:72f65de312a3f10900d7685557f84cb61a9733c50dcc27271a39f5b0051bf528"}, {file = "jsbeautifier-1.15.4.tar.gz", hash = "sha256:5bb18d9efb9331d825735fbc5360ee8f1aac5e52780042803943aa7f854f7592"}, @@ -1489,14 +1577,13 @@ version = "0.12.0" description = "A Python implementation of the JSON5 data format." optional = false python-versions = ">=3.8.0" -groups = ["dev"] files = [ {file = "json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db"}, {file = "json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a"}, ] [package.extras] -dev = ["build (==1.2.2.post1)", "coverage (==7.5.4) ; python_version < \"3.9\"", "coverage (==7.8.0) ; python_version >= \"3.9\"", "mypy (==1.14.1) ; python_version < \"3.9\"", "mypy (==1.15.0) ; python_version >= \"3.9\"", "pip (==25.0.1)", "pylint (==3.2.7) ; python_version < \"3.9\"", "pylint (==3.3.6) ; python_version >= \"3.9\"", "ruff (==0.11.2)", "twine (==6.1.0)", "uv (==0.6.11)"] +dev = ["build (==1.2.2.post1)", "coverage (==7.5.4)", "coverage (==7.8.0)", "mypy (==1.14.1)", "mypy (==1.15.0)", "pip (==25.0.1)", "pylint (==3.2.7)", "pylint (==3.3.6)", "ruff (==0.11.2)", "twine (==6.1.0)", "uv (==0.6.11)"] [[package]] name = "jsonpatch" @@ -1504,7 +1591,6 @@ version = "1.33" description = "Apply JSON-Patches (RFC 6902)" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" -groups = ["main"] files = [ {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, @@ -1519,7 +1605,6 @@ version = "3.0.0" description = "Identify specific nodes in a JSON document (RFC 6901)" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, @@ -1531,7 +1616,6 @@ version = "0.3.27" description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.9" -groups = ["main"] files = [ {file = "langchain-0.3.27-py3-none-any.whl", hash = "sha256:7b20c4f338826acb148d885b20a73a16e410ede9ee4f19bb02011852d5f98798"}, {file = "langchain-0.3.27.tar.gz", hash = "sha256:aa6f1e6274ff055d0fd36254176770f356ed0a8994297d1df47df341953cec62"}, @@ -1571,7 +1655,6 @@ version = "0.3.27" description = "Community contributed LangChain integrations." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "langchain_community-0.3.27-py3-none-any.whl", hash = "sha256:581f97b795f9633da738ea95da9cb78f8879b538090c9b7a68c0aed49c828f0d"}, {file = "langchain_community-0.3.27.tar.gz", hash = "sha256:e1037c3b9da0c6d10bf06e838b034eb741e016515c79ef8f3f16e53ead33d882"}, @@ -1597,7 +1680,6 @@ version = "0.3.72" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "langchain_core-0.3.72-py3-none-any.whl", hash = "sha256:9fa15d390600eb6b6544397a7aa84be9564939b6adf7a2b091179ea30405b240"}, {file = "langchain_core-0.3.72.tar.gz", hash = "sha256:4de3828909b3d7910c313242ab07b241294650f5cb6eac17738dd3638b1cd7de"}, @@ -1618,7 +1700,6 @@ version = "0.3.9" description = "LangChain text splitting utilities" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "langchain_text_splitters-0.3.9-py3-none-any.whl", hash = "sha256:cee0bb816211584ea79cc79927317c358543f40404bcfdd69e69ba3ccde54401"}, {file = "langchain_text_splitters-0.3.9.tar.gz", hash = "sha256:7cd1e5a3aaf609979583eeca2eb34177622570b8fa8f586a605c6b1c34e7ebdb"}, @@ -1633,7 +1714,6 @@ version = "0.4.11" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "langsmith-0.4.11-py3-none-any.whl", hash = "sha256:ce3a52809d37854bcb129affd8cd883025985891b82a46f2ae597c511283360d"}, {file = "langsmith-0.4.11.tar.gz", hash = "sha256:2cb39a8af795ff4014e60ca07a73a9bcd89b67837193c9f6be22efb5c9c4c846"}, @@ -1661,7 +1741,6 @@ version = "5.4.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"}, {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"}, @@ -1810,7 +1889,6 @@ version = "3.8.2" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24"}, {file = "markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45"}, @@ -1826,7 +1904,6 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -1897,7 +1974,6 @@ version = "3.26.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c"}, {file = "marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"}, @@ -1917,7 +1993,6 @@ version = "6.6.3" description = "multidict implementation" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817"}, {file = "multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140"}, @@ -2037,7 +2112,6 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -2049,7 +2123,6 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -2061,7 +2134,6 @@ version = "2.3.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.11" -groups = ["main"] files = [ {file = "numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9"}, {file = "numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168"}, @@ -2139,13 +2211,28 @@ files = [ {file = "numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48"}, ] +[[package]] +name = "oauthlib" +version = "3.3.1" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, + {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + [[package]] name = "openai" version = "1.98.0" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "openai-1.98.0-py3-none-any.whl", hash = "sha256:b99b794ef92196829120e2df37647722104772d2a74d08305df9ced5f26eae34"}, {file = "openai-1.98.0.tar.gz", hash = "sha256:3ee0fcc50ae95267fd22bd1ad095ba5402098f3df2162592e68109999f685427"}, @@ -2173,8 +2260,6 @@ version = "3.11.1" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "orjson-3.11.1-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:92d771c492b64119456afb50f2dff3e03a2db8b5af0eba32c5932d306f970532"}, {file = "orjson-3.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0085ef83a4141c2ed23bfec5fecbfdb1e95dd42fc8e8c76057bdeeec1608ea65"}, @@ -2267,7 +2352,6 @@ version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "test"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -2279,7 +2363,6 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -2291,7 +2374,6 @@ version = "0.4.1" description = "pgvector support for Python" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "pgvector-0.4.1-py3-none-any.whl", hash = "sha256:34bb4e99e1b13d08a2fe82dda9f860f15ddcd0166fbb25bffe15821cbfeb7362"}, {file = "pgvector-0.4.1.tar.gz", hash = "sha256:83d3a1c044ff0c2f1e95d13dfb625beb0b65506cfec0941bfe81fd0ad44f4003"}, @@ -2306,7 +2388,6 @@ version = "11.3.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, @@ -2422,7 +2503,7 @@ fpx = ["olefile"] mic = ["olefile"] test-arrow = ["pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions ; python_version < \"3.10\""] +typing = ["typing-extensions"] xmp = ["defusedxml"] [[package]] @@ -2431,7 +2512,6 @@ version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, @@ -2448,7 +2528,6 @@ version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" -groups = ["test"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -2464,7 +2543,6 @@ version = "4.2.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, @@ -2483,7 +2561,6 @@ version = "0.3.2" description = "Accelerated property cache" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770"}, {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3"}, @@ -2585,13 +2662,47 @@ files = [ {file = "propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168"}, ] +[[package]] +name = "proto-plus" +version = "1.26.1" +description = "Beautiful, Pythonic protocol buffers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, + {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, +] + +[package.dependencies] +protobuf = ">=3.19.0,<7.0.0" + +[package.extras] +testing = ["google-api-core (>=1.31.5)"] + +[[package]] +name = "protobuf" +version = "6.31.1" +description = "" +optional = false +python-versions = ">=3.9" +files = [ + {file = "protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9"}, + {file = "protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447"}, + {file = "protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402"}, + {file = "protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39"}, + {file = "protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6"}, + {file = "protobuf-6.31.1-cp39-cp39-win32.whl", hash = "sha256:0414e3aa5a5f3ff423828e1e6a6e907d6c65c1d5b7e6e975793d5590bdeecc16"}, + {file = "protobuf-6.31.1-cp39-cp39-win_amd64.whl", hash = "sha256:8764cf4587791e7564051b35524b72844f845ad0bb011704c3736cce762d8fe9"}, + {file = "protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e"}, + {file = "protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a"}, +] + [[package]] name = "psycopg2-binary" version = "2.9.10" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, @@ -2663,13 +2774,37 @@ files = [ {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"}, ] +[[package]] +name = "pyasn1" +version = "0.6.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, +] + +[package.dependencies] +pyasn1 = ">=0.6.1,<0.7.0" + [[package]] name = "pycparser" version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -2681,7 +2816,6 @@ version = "2.11.7" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, @@ -2695,7 +2829,7 @@ typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] +timezone = ["tzdata"] [[package]] name = "pydantic-core" @@ -2703,7 +2837,6 @@ version = "2.33.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, @@ -2815,7 +2948,6 @@ version = "2.10.1" description = "Settings management using Pydantic" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796"}, {file = "pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee"}, @@ -2839,7 +2971,6 @@ version = "2.7.0" description = "Use the full Github API v3" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "pygithub-2.7.0-py3-none-any.whl", hash = "sha256:40ecbfe26dc55cc34ab4b0ffa1d455e6f816ef9a2bc8d6f5ad18ce572f163700"}, {file = "pygithub-2.7.0.tar.gz", hash = "sha256:7cd6eafabb09b5369afba3586d86b1f1ad6f1326d2ff01bc47bb26615dce4cbb"}, @@ -2858,7 +2989,6 @@ version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -2873,7 +3003,6 @@ version = "2.10.1" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, @@ -2894,7 +3023,6 @@ version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, @@ -2915,13 +3043,26 @@ cffi = ">=1.4.1" docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] +[[package]] +name = "pyparsing" +version = "3.2.3" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, + {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + [[package]] name = "pytest" version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" -groups = ["test"] files = [ {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, @@ -2943,7 +3084,6 @@ version = "6.2.1" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" -groups = ["test"] files = [ {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, @@ -2963,7 +3103,6 @@ version = "4.11.1" description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10"}, {file = "pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991"}, @@ -2982,7 +3121,6 @@ version = "3.14.1" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, @@ -3000,7 +3138,6 @@ version = "3.8.0" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.9" -groups = ["test"] files = [ {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, @@ -3021,7 +3158,6 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -3036,7 +3172,6 @@ version = "1.1.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.9" -groups = ["main", "test"] files = [ {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, @@ -3051,7 +3186,6 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -3114,7 +3248,6 @@ version = "3.13.0" description = "rapid fuzzy string matching" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "rapidfuzz-3.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aafc42a1dc5e1beeba52cd83baa41372228d6d8266f6d803c16dbabbcc156255"}, {file = "rapidfuzz-3.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:85c9a131a44a95f9cac2eb6e65531db014e09d89c4f18c7b1fa54979cb9ff1f3"}, @@ -3221,7 +3354,6 @@ version = "6.3.0" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "redis-6.3.0-py3-none-any.whl", hash = "sha256:92f079d656ded871535e099080f70fab8e75273c0236797126ac60242d638e9b"}, {file = "redis-6.3.0.tar.gz", hash = "sha256:3000dbe532babfb0999cdab7b3e5744bcb23e51923febcfaeb52c8cfb29632ef"}, @@ -3238,7 +3370,6 @@ version = "2025.7.34" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "regex-2025.7.34-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d856164d25e2b3b07b779bfed813eb4b6b6ce73c2fd818d46f47c1eb5cd79bd6"}, {file = "regex-2025.7.34-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d15a9da5fad793e35fb7be74eec450d968e05d2e294f3e0e77ab03fa7234a83"}, @@ -3335,7 +3466,6 @@ version = "4.4.3" description = "The Reportlab Toolkit" optional = false python-versions = "<4,>=3.7" -groups = ["main"] files = [ {file = "reportlab-4.4.3-py3-none-any.whl", hash = "sha256:df905dc5ec5ddaae91fc9cb3371af863311271d555236410954961c5ee6ee1b5"}, {file = "reportlab-4.4.3.tar.gz", hash = "sha256:073b0975dab69536acd3251858e6b0524ed3e087e71f1d0d1895acb50acf9c7b"}, @@ -3358,7 +3488,6 @@ version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, @@ -3374,13 +3503,30 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=3.4" +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + [[package]] name = "requests-toolbelt" version = "1.0.0" description = "A utility belt for advanced users of python-requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] files = [ {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, @@ -3389,13 +3535,26 @@ files = [ [package.dependencies] requests = ">=2.0.1,<3.0.0" +[[package]] +name = "rsa" +version = "4.9.1" +description = "Pure-Python RSA implementation" +optional = false +python-versions = "<4,>=3.6" +files = [ + {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, + {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + [[package]] name = "ruff" version = "0.11.13" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" -groups = ["dev"] files = [ {file = "ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46"}, {file = "ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48"}, @@ -3423,7 +3582,6 @@ version = "0.13.1" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724"}, {file = "s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf"}, @@ -3441,7 +3599,6 @@ version = "2.34.1" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "sentry_sdk-2.34.1-py2.py3-none-any.whl", hash = "sha256:b7a072e1cdc5abc48101d5146e1ae680fa81fe886d8d95aaa25a0b450c818d32"}, {file = "sentry_sdk-2.34.1.tar.gz", hash = "sha256:69274eb8c5c38562a544c3e9f68b5be0a43be4b697f5fd385bf98e4fbe672687"}, @@ -3499,7 +3656,6 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -3511,7 +3667,6 @@ version = "1.23.0" description = "The Bolt Framework for Python" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "slack_bolt-1.23.0-py2.py3-none-any.whl", hash = "sha256:6d6ae39d80c964c362505ae4e587eed2b26dbc3a9f0cb76af1150c30fb670488"}, {file = "slack_bolt-1.23.0.tar.gz", hash = "sha256:3d2c3eb13131407a94f925eb22b180d352c2d97b808303ef92b7a46d6508c843"}, @@ -3526,7 +3681,6 @@ version = "3.36.0" description = "The Slack API Platform SDK for Python" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "slack_sdk-3.36.0-py2.py3-none-any.whl", hash = "sha256:6c96887d7175fc1b0b2777b73bb65f39b5b8bee9bd8acfec071d64014f9e2d10"}, {file = "slack_sdk-3.36.0.tar.gz", hash = "sha256:8586022bdbdf9f8f8d32f394540436c53b1e7c8da9d21e1eab4560ba70cfcffa"}, @@ -3541,7 +3695,6 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -3553,7 +3706,6 @@ version = "2.0.42" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "SQLAlchemy-2.0.42-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ee065898359fdee83961aed5cf1fb4cfa913ba71b58b41e036001d90bebbf7a"}, {file = "SQLAlchemy-2.0.42-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56bc76d86216443daa2e27e6b04a9b96423f0b69b5d0c40c7f4b9a4cdf7d8d90"}, @@ -3649,7 +3801,6 @@ version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, @@ -3665,7 +3816,6 @@ version = "0.270.6" description = "A library for creating GraphQL APIs" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "strawberry_graphql-0.270.6-py3-none-any.whl", hash = "sha256:e51e5cba074a0c19267f729e2aaf8782dfda7932a8deb9c256e8eea7a88ea419"}, {file = "strawberry_graphql-0.270.6.tar.gz", hash = "sha256:1a41963ae0f421ae4f9ec078551422b114273597caf999a4cb4091b6955003ac"}, @@ -3690,7 +3840,7 @@ debug-server = ["libcst (>=0.4.7,<1.8.0)", "pygments (>=2.3,<3.0)", "python-mult django = ["Django (>=3.2)", "asgiref (>=3.2,<4.0)"] fastapi = ["fastapi (>=0.65.2)", "python-multipart (>=0.0.7)"] flask = ["flask (>=1.1)"] -litestar = ["litestar (>=2) ; python_version >= \"3.10\" and python_version < \"4.0\""] +litestar = ["litestar (>=2)"] opentelemetry = ["opentelemetry-api (<2)", "opentelemetry-sdk (<2)"] pydantic = ["pydantic (>1.6.1)"] pyinstrument = ["pyinstrument (>=4.0.0)"] @@ -3703,7 +3853,6 @@ version = "0.59.1" description = "Strawberry GraphQL Django extension" optional = false python-versions = "<4.0,>=3.9" -groups = ["main"] files = [ {file = "strawberry_graphql_django-0.59.1-py3-none-any.whl", hash = "sha256:c0d74f6f2f140d8683fd3ac7405731ed63342d3aa9294443efe2412e51379227"}, {file = "strawberry_graphql_django-0.59.1.tar.gz", hash = "sha256:b886f1371539962f286b43d273fd91505f943e5e6688f191d6113576b40070ad"}, @@ -3724,7 +3873,6 @@ version = "9.1.2" description = "Retry code until it succeeds" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"}, {file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"}, @@ -3740,7 +3888,6 @@ version = "0.22.1" description = "Fuzzy string matching in python" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481"}, {file = "thefuzz-0.22.1.tar.gz", hash = "sha256:7138039a7ecf540da323792d8592ef9902b1d79eb78c147d4f20664de79f3680"}, @@ -3755,7 +3902,6 @@ version = "4.67.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" -groups = ["main", "dev"] files = [ {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, @@ -3777,7 +3923,6 @@ version = "4.14.1" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, @@ -3789,7 +3934,6 @@ version = "0.9.0" description = "Runtime inspection utilities for typing module." optional = false python-versions = "*" -groups = ["main"] files = [ {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, @@ -3805,7 +3949,6 @@ version = "0.4.1" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, @@ -3820,27 +3963,35 @@ version = "2025.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" -groups = ["main"] -markers = "sys_platform == \"win32\"" files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, ] +[[package]] +name = "uritemplate" +version = "4.2.0" +description = "Implementation of RFC 6570 URI Templates" +optional = false +python-versions = ">=3.9" +files = [ + {file = "uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686"}, + {file = "uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e"}, +] + [[package]] name = "urllib3" version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -3851,7 +4002,6 @@ version = "20.33.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "virtualenv-20.33.0-py3-none-any.whl", hash = "sha256:106b6baa8ab1b526d5a9b71165c85c456fbd49b16976c88e2bc9352ee3bc5d3f"}, {file = "virtualenv-20.33.0.tar.gz", hash = "sha256:47e0c0d2ef1801fce721708ccdf2a28b9403fa2307c3268aebd03225976f61d2"}, @@ -3864,7 +4014,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] name = "yarl" @@ -3872,7 +4022,6 @@ version = "1.20.1" description = "Yet another URL library" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4"}, {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a"}, @@ -3991,7 +4140,6 @@ version = "0.23.0" description = "Zstandard bindings for Python" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"}, {file = "zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"}, @@ -4099,6 +4247,6 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\ cffi = ["cffi (>=1.11)"] [metadata] -lock-version = "2.1" +lock-version = "2.0" python-versions = "^3.13" -content-hash = "83d0a2eb8375b51ef3f234cabd5a1568c68fa67ec20bf3916b8f2baf097ea31e" +content-hash = "d76497e2278f6e1f53fb1e985a20d52c3ada777d9d3573e8adff3d42ddf74192" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index c66ad810fe..878d72a88b 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -55,6 +55,9 @@ slack-sdk = "^3.35.0" strawberry-graphql = { extras = ["django"], version = "^0.270.1" } strawberry-graphql-django = "^0.59.1" thefuzz = "^0.22.1" +google-api-python-client = "^2.178.0" +google-auth-httplib2 = "^0.2.0" +google-auth-oauthlib = "^1.2.2" [tool.poetry.group.dev.dependencies] djlint = "^1.36.4" From af62c4731fb23b34eb04a8ae3fea40857ac68be6 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 7 Aug 2025 19:25:04 +0300 Subject: [PATCH 02/25] Add model --- backend/apps/slack/models/__init__.py | 1 + .../apps/slack/models/google_slack_auth.py | 93 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 backend/apps/slack/models/google_slack_auth.py diff --git a/backend/apps/slack/models/__init__.py b/backend/apps/slack/models/__init__.py index 3bbe0878de..073dcb23d7 100644 --- a/backend/apps/slack/models/__init__.py +++ b/backend/apps/slack/models/__init__.py @@ -1,5 +1,6 @@ from .conversation import Conversation from .event import Event +from .google_slack_auth import GoogleSlackAuth from .member import Member from .message import Message from .workspace import Workspace diff --git a/backend/apps/slack/models/google_slack_auth.py b/backend/apps/slack/models/google_slack_auth.py new file mode 100644 index 0000000000..fac45b25af --- /dev/null +++ b/backend/apps/slack/models/google_slack_auth.py @@ -0,0 +1,93 @@ +"""Slack Google OAuth Authentication Model.""" + +import os + +from django.db import models +from django.utils import timezone +from google_auth_oauthlib.flow import Flow + + +class GoogleSlackAuth(models.Model): + """Model to store Google OAuth tokens for Slack integration.""" + + user = models.OneToOneField( + "slack.Member", + on_delete=models.CASCADE, + related_name="google_slack_auth", + verbose_name="Slack Member", + ) + access_token = models.CharField( + max_length=255, + verbose_name="Access Token", + ) + refresh_token = models.CharField( + max_length=255, + verbose_name="Refresh Token", + blank=True, + ) + expires_at = models.DateTimeField( + verbose_name="Token Expiry", + blank=True, + null=True, + ) + + @staticmethod + def get_flow(): + """Create a Google OAuth flow instance.""" + return Flow.from_client_config( + client_config={ + "web": { + "client_id": os.getenv("GOOGLE_AUTH_CLIENT_ID"), + "client_secret": os.getenv("GOOGLE_AUTH_CLIENT_SECRET"), + "redirect_uris": [os.getenv("GOOGLE_AUTH_REDIRECT_URI")], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + } + }, + scopes=["https://www.googleapis.com/auth/calendar.readonly"], + ) + + @staticmethod + def authenticate(auth_url, user): + """Authenticate a user and return a GoogleSlackAuth instance.""" + auth = GoogleSlackAuth.objects.get_or_create(user=user)[0] + if auth.access_token and not auth.is_token_expired: + return auth + if auth.access_token: + GoogleSlackAuth.refresh_access_token(auth) + return auth + flow = GoogleSlackAuth.get_flow() + flow.redirect_uri = os.getenv("GOOGLE_AUTH_REDIRECT_URI") + flow.fetch_token(authorization_response=auth_url) + auth.access_token = flow.credentials.token + auth.refresh_token = flow.credentials.refresh_token + auth.expires_at = flow.credentials.expiry + auth.save() + return auth + + @property + def is_token_expired(self): + """Check if the access token is expired.""" + return self.expires_at is None or self.expires_at <= timezone.now() + + @staticmethod + def refresh_access_token(auth): + """Refresh the access token using the refresh token.""" + if not auth.refresh_token: + raise ValueError("No refresh token available to refresh access token.") + + flow = GoogleSlackAuth.get_flow() + flow.fetch_token( + refresh_token=auth.refresh_token, + client_id=os.getenv("GOOGLE_AUTH_CLIENT_ID"), + client_secret=os.getenv("GOOGLE_AUTH_CLIENT_SECRET"), + ) + + credentials = flow.credentials + auth.access_token = credentials.token + auth.refresh_token = credentials.refresh_token + auth.expires_at = credentials.expiry + auth.save() + + def __str__(self): + return f"GoogleSlackAuth(user={self.user})" From 90874decea200557ffbcac4f3bf68b8f83cb003f Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 9 Aug 2025 11:33:36 +0300 Subject: [PATCH 03/25] Update logic and add tests --- backend/.env.example | 3 + backend/apps/slack/models/__init__.py | 2 +- .../{google_slack_auth.py => google_auth.py} | 45 ++- backend/settings/base.py | 7 + .../apps/slack/models/google_auth_test.py | 311 ++++++++++++++++++ 5 files changed, 354 insertions(+), 14 deletions(-) rename backend/apps/slack/models/{google_slack_auth.py => google_auth.py} (60%) create mode 100644 backend/tests/apps/slack/models/google_auth_test.py diff --git a/backend/.env.example b/backend/.env.example index 2916e696d1..c6714d414b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -20,3 +20,6 @@ DJANGO_SENTRY_DSN=None DJANGO_SLACK_BOT_TOKEN=None DJANGO_SLACK_SIGNING_SECRET=None GITHUB_TOKEN=None +GOOGLE_AUTH_CLIENT_ID=None +GOOGLE_AUTH_CLIENT_SECRET=None +GOOGLE_AUTH_REDIRECT_URI=http://localhost:8000/integrations/slack/oauth2/callback/ diff --git a/backend/apps/slack/models/__init__.py b/backend/apps/slack/models/__init__.py index 073dcb23d7..a51e1a3467 100644 --- a/backend/apps/slack/models/__init__.py +++ b/backend/apps/slack/models/__init__.py @@ -1,6 +1,6 @@ from .conversation import Conversation from .event import Event -from .google_slack_auth import GoogleSlackAuth +from .google_auth import GoogleAuth from .member import Member from .message import Message from .workspace import Workspace diff --git a/backend/apps/slack/models/google_slack_auth.py b/backend/apps/slack/models/google_auth.py similarity index 60% rename from backend/apps/slack/models/google_slack_auth.py rename to backend/apps/slack/models/google_auth.py index fac45b25af..a1d5e538a8 100644 --- a/backend/apps/slack/models/google_slack_auth.py +++ b/backend/apps/slack/models/google_auth.py @@ -2,18 +2,27 @@ import os +from django.conf import settings from django.db import models from django.utils import timezone from google_auth_oauthlib.flow import Flow +GOOGLE_AUTH_CLIENT_ID = settings.GOOGLE_AUTH_CLIENT_ID +GOOGLE_AUTH_CLIENT_SECRET = settings.GOOGLE_AUTH_CLIENT_SECRET +GOOGLE_AUTH_REDIRECT_URI = settings.GOOGLE_AUTH_REDIRECT_URI -class GoogleSlackAuth(models.Model): +error_message = ( + "Google OAuth client ID, secret, and redirect URI must be set in environment variables." +) + + +class GoogleAuth(models.Model): """Model to store Google OAuth tokens for Slack integration.""" user = models.OneToOneField( "slack.Member", on_delete=models.CASCADE, - related_name="google_slack_auth", + related_name="google_auth", verbose_name="Slack Member", ) access_token = models.CharField( @@ -34,12 +43,14 @@ class GoogleSlackAuth(models.Model): @staticmethod def get_flow(): """Create a Google OAuth flow instance.""" + if not settings.IS_GOOGLE_AUTH_ENABLED: + raise ValueError(error_message) return Flow.from_client_config( client_config={ "web": { - "client_id": os.getenv("GOOGLE_AUTH_CLIENT_ID"), - "client_secret": os.getenv("GOOGLE_AUTH_CLIENT_SECRET"), - "redirect_uris": [os.getenv("GOOGLE_AUTH_REDIRECT_URI")], + "client_id": GOOGLE_AUTH_CLIENT_ID, + "client_secret": GOOGLE_AUTH_CLIENT_SECRET, + "redirect_uris": [GOOGLE_AUTH_REDIRECT_URI], "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", } @@ -49,15 +60,19 @@ def get_flow(): @staticmethod def authenticate(auth_url, user): - """Authenticate a user and return a GoogleSlackAuth instance.""" - auth = GoogleSlackAuth.objects.get_or_create(user=user)[0] + """Authenticate a user and return a GoogleAuth instance.""" + if not settings.IS_GOOGLE_AUTH_ENABLED: + raise ValueError(error_message) + auth = GoogleAuth.objects.get_or_create(user=user)[0] if auth.access_token and not auth.is_token_expired: return auth if auth.access_token: - GoogleSlackAuth.refresh_access_token(auth) + # If the access token is present but expired, refresh it + GoogleAuth.refresh_access_token(auth) return auth - flow = GoogleSlackAuth.get_flow() - flow.redirect_uri = os.getenv("GOOGLE_AUTH_REDIRECT_URI") + # This is the first time authentication, so we need to fetch a new token + flow = GoogleAuth.get_flow() + flow.redirect_uri = settings.GOOGLE_AUTH_REDIRECT_URI flow.fetch_token(authorization_response=auth_url) auth.access_token = flow.credentials.token auth.refresh_token = flow.credentials.refresh_token @@ -73,10 +88,13 @@ def is_token_expired(self): @staticmethod def refresh_access_token(auth): """Refresh the access token using the refresh token.""" + if not settings.IS_GOOGLE_AUTH_ENABLED: + raise ValueError(error_message) + refresh_error = "Google OAuth refresh token is not set or expired." if not auth.refresh_token: - raise ValueError("No refresh token available to refresh access token.") + raise ValueError(refresh_error) - flow = GoogleSlackAuth.get_flow() + flow = GoogleAuth.get_flow() flow.fetch_token( refresh_token=auth.refresh_token, client_id=os.getenv("GOOGLE_AUTH_CLIENT_ID"), @@ -90,4 +108,5 @@ def refresh_access_token(auth): auth.save() def __str__(self): - return f"GoogleSlackAuth(user={self.user})" + """Return a string representation of the GoogleAuth instance.""" + return f"GoogleAuth(user={self.user})" diff --git a/backend/settings/base.py b/backend/settings/base.py index 519fc08c5f..1a1b6f3c20 100644 --- a/backend/settings/base.py +++ b/backend/settings/base.py @@ -18,6 +18,13 @@ class Base(Configuration): CORS_ALLOW_CREDENTIALS = True DEBUG = False + GOOGLE_AUTH_CLIENT_ID = os.getenv("GOOGLE_AUTH_CLIENT_ID") + GOOGLE_AUTH_CLIENT_SECRET = os.getenv("GOOGLE_AUTH_CLIENT_SECRET") + GOOGLE_AUTH_REDIRECT_URI = os.getenv("GOOGLE_AUTH_REDIRECT_URI") + + IS_GOOGLE_AUTH_ENABLED = ( + GOOGLE_AUTH_CLIENT_ID and GOOGLE_AUTH_CLIENT_SECRET and GOOGLE_AUTH_REDIRECT_URI + ) IS_LOCAL_ENVIRONMENT = False IS_PRODUCTION_ENVIRONMENT = False IS_STAGING_ENVIRONMENT = False diff --git a/backend/tests/apps/slack/models/google_auth_test.py b/backend/tests/apps/slack/models/google_auth_test.py new file mode 100644 index 0000000000..6eb04118d0 --- /dev/null +++ b/backend/tests/apps/slack/models/google_auth_test.py @@ -0,0 +1,311 @@ +"""Tests for GoogleAuth model.""" + +from datetime import timedelta +from unittest.mock import Mock, patch + +import pytest +from django.test import override_settings +from django.utils import timezone +from google_auth_oauthlib.flow import Flow + +from apps.slack.models.google_auth import GoogleAuth +from apps.slack.models.member import Member + + +class TestGoogleAuthModel: + """Test cases for GoogleAuth model.""" + + @pytest.fixture(autouse=True) + def setUp(self): + """Set up test data.""" + self.member = Member(slack_user_id="U123456789", username="testuser") + self.valid_token = "valid_access_token" + self.valid_refresh_token = "valid_refresh_token" + self.expired_time = timezone.now() - timedelta(hours=1) + self.future_time = timezone.now() + timedelta(hours=1) + + def test_google_auth_creation(self): + """Test GoogleAuth model creation.""" + auth = GoogleAuth( + user=self.member, + access_token=self.valid_token, + refresh_token=self.valid_refresh_token, + expires_at=self.future_time, + ) + + assert auth.user == self.member + assert auth.access_token == self.valid_token + assert auth.refresh_token == self.valid_refresh_token + assert auth.expires_at == self.future_time + + def test_string_representation(self): + """Test string representation of GoogleAuth.""" + auth = GoogleAuth(user=self.member, access_token=self.valid_token) + + expected = f"GoogleAuth(user={self.member})" + assert str(auth) == expected + + def test_one_to_one_relationship(self): + """Test one-to-one relationship with Member.""" + auth = GoogleAuth(user=self.member, access_token=self.valid_token) + + assert self.member.google_auth == auth + assert auth.user == self.member + + def test_is_token_expired_with_future_expiry(self): + """Test is_token_expired property with future expiry.""" + auth = GoogleAuth( + user=self.member, access_token=self.valid_token, expires_at=self.future_time + ) + + assert not auth.is_token_expired + + def test_is_token_expired_with_past_expiry(self): + """Test is_token_expired property with past expiry.""" + auth = GoogleAuth( + user=self.member, access_token=self.valid_token, expires_at=self.expired_time + ) + + assert auth.is_token_expired + + def test_is_token_expired_with_none_expiry(self): + """Test is_token_expired property with None expiry.""" + auth = GoogleAuth(user=self.member, access_token=self.valid_token, expires_at=None) + + assert auth.is_token_expired + + @override_settings(IS_GOOGLE_AUTH_ENABLED=False) + def test_get_flow_when_disabled(self): + """Test get_flow raises error when Google auth is disabled.""" + with pytest.raises(ValueError, match="Google OAuth client ID"): + GoogleAuth.get_flow() + + @patch.dict( + "os.environ", + { + "GOOGLE_AUTH_CLIENT_ID": "test_client_id", + "GOOGLE_AUTH_CLIENT_SECRET": "test_client_secret", + "GOOGLE_AUTH_REDIRECT_URI": "http://localhost:8000/callback", + }, + ) + @override_settings(IS_GOOGLE_AUTH_ENABLED=True) + @patch("apps.slack.models.google_auth.Flow.from_client_config") + def test_get_flow_success(self, mock_flow): + """Test successful get_flow creation.""" + mock_flow_instance = Mock(spec=Flow) + mock_flow.return_value = mock_flow_instance + + result = GoogleAuth.get_flow() + + assert result == mock_flow_instance + + @override_settings(IS_GOOGLE_AUTH_ENABLED=False) + def test_authenticate_when_disabled(self): + """Test authenticate raises error when Google auth is disabled.""" + with pytest.raises(ValueError, match="Google OAuth client ID"): + GoogleAuth.authenticate("http://auth.url", self.member) + + @patch.dict( + "os.environ", + { + "GOOGLE_AUTH_CLIENT_ID": "test_client_id", + "GOOGLE_AUTH_CLIENT_SECRET": "test_client_secret", + "GOOGLE_AUTH_REDIRECT_URI": "http://localhost:8000/callback", + }, + ) + @patch("apps.slack.models.google_auth.GoogleAuth.save") + @patch("apps.slack.models.google_auth.GoogleAuth.objects.get_or_create") + @patch("apps.slack.models.google_auth.GoogleAuth.get_flow") + def test_authenticate_existing_valid_token(self, mock_get_flow, mock_get_or_create, mock_save): + """Test authenticate with existing valid token.""" + # Create existing auth with valid token + + result = GoogleAuth.authenticate("http://auth.url", self.member) + mock_get_flow.return_value = Mock(spec=Flow) + + assert result.access_token == self.valid_token + assert result.refresh_token == self.valid_refresh_token + assert result.expires_at == self.future_time + mock_get_or_create.assert_called_once_with(user=self.member) + mock_save.assert_not_called() + + @patch.dict( + "os.environ", + { + "GOOGLE_AUTH_CLIENT_ID": "test_client_id", + "GOOGLE_AUTH_CLIENT_SECRET": "test_client_secret", + "GOOGLE_AUTH_REDIRECT_URI": "http://localhost:8000/callback", + }, + ) + @patch("apps.slack.models.google_auth.GoogleAuth.refresh_access_token") + @patch("apps.slack.models.google_auth.GoogleAuth.save") + @patch("apps.slack.models.google_auth.GoogleAuth.objects.get_or_create") + def test_authenticate_existing_expired_token( + self, mock_get_or_create, mock_save, mock_refresh + ): + """Test authenticate with existing expired token.""" + # Create existing auth with expired token + existing_auth = GoogleAuth( + user=self.member, + access_token=self.valid_token, + refresh_token=self.valid_refresh_token, + expires_at=self.expired_time, + ) + + result = GoogleAuth.authenticate("http://auth.url", self.member) + + mock_refresh.assert_called_once_with(existing_auth) + mock_get_or_create.assert_called_once_with(user=self.member) + mock_save.assert_called_once() + + @patch.dict( + "os.environ", + { + "GOOGLE_AUTH_CLIENT_ID": "env_client_id", + "GOOGLE_AUTH_CLIENT_SECRET": "env_client_secret", + "GOOGLE_AUTH_REDIRECT_URI": "http://localhost:8000/callback", + }, + ) + @patch("apps.slack.models.google_auth.GoogleAuth.get_flow") + @patch("apps.slack.models.google_auth.GoogleAuth.save") + @patch("apps.slack.models.google_auth.GoogleAuth.objects.get_or_create") + def test_authenticate_first_time(self, mock_get_or_create, mock_save, mock_get_flow): + """Test authenticate for first time (no existing token).""" + # Mock flow and credentials + mock_credentials = Mock() + mock_credentials.token = "new_access_token" # NOQA + mock_credentials.refresh_token = "new_refresh_token" + mock_credentials.expiry = self.future_time + + mock_flow_instance = Mock() + mock_flow_instance.credentials = mock_credentials + mock_get_flow.return_value = mock_flow_instance + + result = GoogleAuth.authenticate("http://auth.url", self.member) + + assert result.access_token == "new_access_token" + assert result.refresh_token == "new_refresh_token" + assert result.expires_at == self.future_time + mock_get_or_create.assert_called_once_with(user=self.member) + + mock_flow_instance.fetch_token.assert_called_once_with( + refresh_token=self.valid_refresh_token, + client_id="env_client_id", + client_secret="env_client_secret", + ) + mock_save.assert_called_once() + + @override_settings(IS_GOOGLE_AUTH_ENABLED=False) + def test_refresh_access_token_when_disabled(self): + """Test refresh_access_token raises error when Google auth is disabled.""" + auth = GoogleAuth( + user=self.member, access_token=self.valid_token, refresh_token=self.valid_refresh_token + ) + + with pytest.raises(ValueError, match="Google OAuth client ID"): + GoogleAuth.refresh_access_token(auth) + + @patch.dict( + "os.environ", + { + "GOOGLE_AUTH_CLIENT_ID": "env_client_id", + "GOOGLE_AUTH_CLIENT_SECRET": "env_client_secret", + "GOOGLE_AUTH_REDIRECT_URI": "http://localhost:8000/callback", + }, + ) + @patch("apps.slack.models.google_auth.GoogleAuth.get_flow") + @patch("apps.slack.models.google_auth.GoogleAuth.save") + def test_refresh_access_token_success(self, mock_save, mock_get_flow): + """Test successful refresh_access_token.""" + # Create auth with refresh token + auth = GoogleAuth( + user=self.member, + access_token=self.valid_token, + refresh_token=self.valid_refresh_token, + expires_at=self.expired_time, + ) + + # Mock flow and new credentials + mock_credentials = Mock() + mock_credentials.token = "new_access_token" + mock_credentials.refresh_token = "new_refresh_token" + mock_credentials.expiry = self.future_time + + mock_flow_instance = Mock() + mock_flow_instance.credentials = mock_credentials + mock_get_flow.return_value = mock_flow_instance + + GoogleAuth.refresh_access_token(auth) + + assert auth.access_token == "new_access_token" + assert auth.refresh_token == "new_refresh_token" + assert auth.expires_at == self.future_time + + mock_flow_instance.fetch_token.assert_called_once_with( + refresh_token=self.valid_refresh_token, + client_id="env_client_id", + client_secret="env_client_secret", + ) + mock_save.assert_called_once() + + def test_verbose_names(self): + """Test model field verbose names.""" + auth = GoogleAuth(user=self.member, access_token=self.valid_token) + + assert auth._meta.get_field("user").verbose_name == "Slack Member" + assert auth._meta.get_field("access_token").verbose_name == "Access Token" + assert auth._meta.get_field("refresh_token").verbose_name == "Refresh Token" + assert auth._meta.get_field("expires_at").verbose_name == "Token Expiry" + + def test_refresh_token_blank_allowed(self): + """Test that refresh_token can be blank.""" + auth = GoogleAuth( + user=self.member, + access_token=self.valid_token, + refresh_token="", # Blank is allowed + ) + + assert auth.refresh_token == "" + + def test_expires_at_null_allowed(self): + """Test that expires_at can be null.""" + auth = GoogleAuth( + user=self.member, + access_token=self.valid_token, + expires_at=None, # Null is allowed + ) + + assert auth.expires_at is None + + +class TestGoogleAuthIntegration: + """Integration tests for GoogleAuth model.""" + + def test_full_authentication_flow(self): + """Test complete authentication flow.""" + member = Member(slack_user_id="U123456789", username="testuser") + + # Test that we can create and use GoogleAuth + with override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="integration_client_id", + GOOGLE_AUTH_CLIENT_SECRET="integration_client_secret", + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", + ): + # Test flow creation doesn't raise errors + with patch("apps.slack.models.google_auth.Flow.from_client_config") as mock_flow: + mock_flow.return_value = Mock(spec=Flow) + flow = GoogleAuth.get_flow() + assert flow is not None + + # Test authentication with valid token + auth = GoogleAuth( + user=member, + access_token="integration_token", + refresh_token="integration_refresh", + expires_at=timezone.now() + timedelta(hours=1), + ) + + assert not auth.is_token_expired + assert str(auth) == f"GoogleAuth(user={member})" + assert member.google_auth == auth From 97ffd257bffec1bba6c12ce112bb1ec3e480c0c2 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 9 Aug 2025 19:35:09 +0300 Subject: [PATCH 04/25] Update tests, apply make-check and suggestions --- backend/apps/slack/models/google_auth.py | 31 ++--- backend/settings/base.py | 11 +- .../apps/slack/models/google_auth_test.py | 129 +++++++++--------- 3 files changed, 84 insertions(+), 87 deletions(-) diff --git a/backend/apps/slack/models/google_auth.py b/backend/apps/slack/models/google_auth.py index a1d5e538a8..7d8e02984d 100644 --- a/backend/apps/slack/models/google_auth.py +++ b/backend/apps/slack/models/google_auth.py @@ -1,17 +1,11 @@ """Slack Google OAuth Authentication Model.""" -import os - from django.conf import settings from django.db import models from django.utils import timezone from google_auth_oauthlib.flow import Flow -GOOGLE_AUTH_CLIENT_ID = settings.GOOGLE_AUTH_CLIENT_ID -GOOGLE_AUTH_CLIENT_SECRET = settings.GOOGLE_AUTH_CLIENT_SECRET -GOOGLE_AUTH_REDIRECT_URI = settings.GOOGLE_AUTH_REDIRECT_URI - -error_message = ( +AUTH_ERROR_MESSAGE = ( "Google OAuth client ID, secret, and redirect URI must be set in environment variables." ) @@ -25,12 +19,11 @@ class GoogleAuth(models.Model): related_name="google_auth", verbose_name="Slack Member", ) - access_token = models.CharField( - max_length=255, + access_token = models.TextField( verbose_name="Access Token", + blank=True, ) - refresh_token = models.CharField( - max_length=255, + refresh_token = models.TextField( verbose_name="Refresh Token", blank=True, ) @@ -44,13 +37,13 @@ class GoogleAuth(models.Model): def get_flow(): """Create a Google OAuth flow instance.""" if not settings.IS_GOOGLE_AUTH_ENABLED: - raise ValueError(error_message) + raise ValueError(AUTH_ERROR_MESSAGE) return Flow.from_client_config( client_config={ "web": { - "client_id": GOOGLE_AUTH_CLIENT_ID, - "client_secret": GOOGLE_AUTH_CLIENT_SECRET, - "redirect_uris": [GOOGLE_AUTH_REDIRECT_URI], + "client_id": settings.GOOGLE_AUTH_CLIENT_ID, + "client_secret": settings.GOOGLE_AUTH_CLIENT_SECRET, + "redirect_uris": [settings.GOOGLE_AUTH_REDIRECT_URI], "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", } @@ -62,7 +55,7 @@ def get_flow(): def authenticate(auth_url, user): """Authenticate a user and return a GoogleAuth instance.""" if not settings.IS_GOOGLE_AUTH_ENABLED: - raise ValueError(error_message) + raise ValueError(AUTH_ERROR_MESSAGE) auth = GoogleAuth.objects.get_or_create(user=user)[0] if auth.access_token and not auth.is_token_expired: return auth @@ -89,7 +82,7 @@ def is_token_expired(self): def refresh_access_token(auth): """Refresh the access token using the refresh token.""" if not settings.IS_GOOGLE_AUTH_ENABLED: - raise ValueError(error_message) + raise ValueError(AUTH_ERROR_MESSAGE) refresh_error = "Google OAuth refresh token is not set or expired." if not auth.refresh_token: raise ValueError(refresh_error) @@ -97,8 +90,8 @@ def refresh_access_token(auth): flow = GoogleAuth.get_flow() flow.fetch_token( refresh_token=auth.refresh_token, - client_id=os.getenv("GOOGLE_AUTH_CLIENT_ID"), - client_secret=os.getenv("GOOGLE_AUTH_CLIENT_SECRET"), + client_id=settings.GOOGLE_AUTH_CLIENT_ID, + client_secret=settings.GOOGLE_AUTH_CLIENT_SECRET, ) credentials = flow.credentials diff --git a/backend/settings/base.py b/backend/settings/base.py index 1a1b6f3c20..17f6f06a52 100644 --- a/backend/settings/base.py +++ b/backend/settings/base.py @@ -18,12 +18,13 @@ class Base(Configuration): CORS_ALLOW_CREDENTIALS = True DEBUG = False - GOOGLE_AUTH_CLIENT_ID = os.getenv("GOOGLE_AUTH_CLIENT_ID") - GOOGLE_AUTH_CLIENT_SECRET = os.getenv("GOOGLE_AUTH_CLIENT_SECRET") - GOOGLE_AUTH_REDIRECT_URI = os.getenv("GOOGLE_AUTH_REDIRECT_URI") + GOOGLE_AUTH_CLIENT_ID = values.Value(environ_name="GOOGLE_AUTH_CLIENT_ID") + GOOGLE_AUTH_CLIENT_SECRET = values.Value(environ_name="GOOGLE_AUTH_CLIENT_SECRET") + GOOGLE_AUTH_REDIRECT_URI = values.Value(environ_name="GOOGLE_AUTH_REDIRECT_URI") - IS_GOOGLE_AUTH_ENABLED = ( - GOOGLE_AUTH_CLIENT_ID and GOOGLE_AUTH_CLIENT_SECRET and GOOGLE_AUTH_REDIRECT_URI + IS_GOOGLE_AUTH_ENABLED = all( + value not in (None, "None", "") + for value in (GOOGLE_AUTH_CLIENT_ID, GOOGLE_AUTH_CLIENT_SECRET, GOOGLE_AUTH_REDIRECT_URI) ) IS_LOCAL_ENVIRONMENT = False IS_PRODUCTION_ENVIRONMENT = False diff --git a/backend/tests/apps/slack/models/google_auth_test.py b/backend/tests/apps/slack/models/google_auth_test.py index 6eb04118d0..fdf4f0d0cb 100644 --- a/backend/tests/apps/slack/models/google_auth_test.py +++ b/backend/tests/apps/slack/models/google_auth_test.py @@ -19,8 +19,8 @@ class TestGoogleAuthModel: def setUp(self): """Set up test data.""" self.member = Member(slack_user_id="U123456789", username="testuser") - self.valid_token = "valid_access_token" - self.valid_refresh_token = "valid_refresh_token" + self.valid_token = "valid_access_token" # noqa: S105 + self.valid_refresh_token = "valid_refresh_token" # noqa: S105 self.expired_time = timezone.now() - timedelta(hours=1) self.future_time = timezone.now() + timedelta(hours=1) @@ -80,13 +80,11 @@ def test_get_flow_when_disabled(self): with pytest.raises(ValueError, match="Google OAuth client ID"): GoogleAuth.get_flow() - @patch.dict( - "os.environ", - { - "GOOGLE_AUTH_CLIENT_ID": "test_client_id", - "GOOGLE_AUTH_CLIENT_SECRET": "test_client_secret", - "GOOGLE_AUTH_REDIRECT_URI": "http://localhost:8000/callback", - }, + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", ) @override_settings(IS_GOOGLE_AUTH_ENABLED=True) @patch("apps.slack.models.google_auth.Flow.from_client_config") @@ -103,15 +101,13 @@ def test_get_flow_success(self, mock_flow): def test_authenticate_when_disabled(self): """Test authenticate raises error when Google auth is disabled.""" with pytest.raises(ValueError, match="Google OAuth client ID"): - GoogleAuth.authenticate("http://auth.url", self.member) - - @patch.dict( - "os.environ", - { - "GOOGLE_AUTH_CLIENT_ID": "test_client_id", - "GOOGLE_AUTH_CLIENT_SECRET": "test_client_secret", - "GOOGLE_AUTH_REDIRECT_URI": "http://localhost:8000/callback", - }, + GoogleAuth.authenticate("http://auth.url", self.member) # NOSONAR + + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", ) @patch("apps.slack.models.google_auth.GoogleAuth.save") @patch("apps.slack.models.google_auth.GoogleAuth.objects.get_or_create") @@ -120,8 +116,17 @@ def test_authenticate_existing_valid_token(self, mock_get_flow, mock_get_or_crea """Test authenticate with existing valid token.""" # Create existing auth with valid token - result = GoogleAuth.authenticate("http://auth.url", self.member) mock_get_flow.return_value = Mock(spec=Flow) + mock_get_or_create.return_value = ( + GoogleAuth( + user=self.member, + access_token=self.valid_token, + refresh_token=self.valid_refresh_token, + expires_at=self.future_time, + ), + True, + ) + result = GoogleAuth.authenticate("http://auth.url", self.member) # NOSONAR assert result.access_token == self.valid_token assert result.refresh_token == self.valid_refresh_token @@ -129,20 +134,15 @@ def test_authenticate_existing_valid_token(self, mock_get_flow, mock_get_or_crea mock_get_or_create.assert_called_once_with(user=self.member) mock_save.assert_not_called() - @patch.dict( - "os.environ", - { - "GOOGLE_AUTH_CLIENT_ID": "test_client_id", - "GOOGLE_AUTH_CLIENT_SECRET": "test_client_secret", - "GOOGLE_AUTH_REDIRECT_URI": "http://localhost:8000/callback", - }, + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", ) @patch("apps.slack.models.google_auth.GoogleAuth.refresh_access_token") - @patch("apps.slack.models.google_auth.GoogleAuth.save") @patch("apps.slack.models.google_auth.GoogleAuth.objects.get_or_create") - def test_authenticate_existing_expired_token( - self, mock_get_or_create, mock_save, mock_refresh - ): + def test_authenticate_existing_expired_token(self, mock_get_or_create, mock_refresh): """Test authenticate with existing expired token.""" # Create existing auth with expired token existing_auth = GoogleAuth( @@ -151,20 +151,18 @@ def test_authenticate_existing_expired_token( refresh_token=self.valid_refresh_token, expires_at=self.expired_time, ) + mock_get_or_create.return_value = (existing_auth, False) - result = GoogleAuth.authenticate("http://auth.url", self.member) + GoogleAuth.authenticate("http://auth.url", self.member) # NOSONAR mock_refresh.assert_called_once_with(existing_auth) mock_get_or_create.assert_called_once_with(user=self.member) - mock_save.assert_called_once() - @patch.dict( - "os.environ", - { - "GOOGLE_AUTH_CLIENT_ID": "env_client_id", - "GOOGLE_AUTH_CLIENT_SECRET": "env_client_secret", - "GOOGLE_AUTH_REDIRECT_URI": "http://localhost:8000/callback", - }, + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", ) @patch("apps.slack.models.google_auth.GoogleAuth.get_flow") @patch("apps.slack.models.google_auth.GoogleAuth.save") @@ -173,25 +171,32 @@ def test_authenticate_first_time(self, mock_get_or_create, mock_save, mock_get_f """Test authenticate for first time (no existing token).""" # Mock flow and credentials mock_credentials = Mock() - mock_credentials.token = "new_access_token" # NOQA - mock_credentials.refresh_token = "new_refresh_token" + mock_credentials.token = "new_access_token" # noqa: S105 # NOSONAR + mock_credentials.refresh_token = "new_refresh_token" # noqa: S105 mock_credentials.expiry = self.future_time mock_flow_instance = Mock() mock_flow_instance.credentials = mock_credentials mock_get_flow.return_value = mock_flow_instance + mock_get_or_create.return_value = ( + GoogleAuth( + user=self.member, + access_token="", + refresh_token="", + expires_at=None, + ), + True, + ) - result = GoogleAuth.authenticate("http://auth.url", self.member) + result = GoogleAuth.authenticate("http://auth.url", self.member) # NOSONAR - assert result.access_token == "new_access_token" - assert result.refresh_token == "new_refresh_token" + assert result.access_token == "new_access_token" # noqa: S105 + assert result.refresh_token == "new_refresh_token" # noqa: S105 assert result.expires_at == self.future_time mock_get_or_create.assert_called_once_with(user=self.member) mock_flow_instance.fetch_token.assert_called_once_with( - refresh_token=self.valid_refresh_token, - client_id="env_client_id", - client_secret="env_client_secret", + authorization_response="http://auth.url", ) mock_save.assert_called_once() @@ -205,13 +210,11 @@ def test_refresh_access_token_when_disabled(self): with pytest.raises(ValueError, match="Google OAuth client ID"): GoogleAuth.refresh_access_token(auth) - @patch.dict( - "os.environ", - { - "GOOGLE_AUTH_CLIENT_ID": "env_client_id", - "GOOGLE_AUTH_CLIENT_SECRET": "env_client_secret", - "GOOGLE_AUTH_REDIRECT_URI": "http://localhost:8000/callback", - }, + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", ) @patch("apps.slack.models.google_auth.GoogleAuth.get_flow") @patch("apps.slack.models.google_auth.GoogleAuth.save") @@ -227,8 +230,8 @@ def test_refresh_access_token_success(self, mock_save, mock_get_flow): # Mock flow and new credentials mock_credentials = Mock() - mock_credentials.token = "new_access_token" - mock_credentials.refresh_token = "new_refresh_token" + mock_credentials.token = "new_access_token" # noqa: S105 # NOSONAR + mock_credentials.refresh_token = "new_refresh_token" # noqa: S105 mock_credentials.expiry = self.future_time mock_flow_instance = Mock() @@ -237,14 +240,14 @@ def test_refresh_access_token_success(self, mock_save, mock_get_flow): GoogleAuth.refresh_access_token(auth) - assert auth.access_token == "new_access_token" - assert auth.refresh_token == "new_refresh_token" + assert auth.access_token == "new_access_token" # noqa: S105 + assert auth.refresh_token == "new_refresh_token" # noqa: S105 assert auth.expires_at == self.future_time mock_flow_instance.fetch_token.assert_called_once_with( refresh_token=self.valid_refresh_token, - client_id="env_client_id", - client_secret="env_client_secret", + client_id="test_client_id", + client_secret="test_client_secret", # noqa: S106 ) mock_save.assert_called_once() @@ -289,7 +292,7 @@ def test_full_authentication_flow(self): with override_settings( IS_GOOGLE_AUTH_ENABLED=True, GOOGLE_AUTH_CLIENT_ID="integration_client_id", - GOOGLE_AUTH_CLIENT_SECRET="integration_client_secret", + GOOGLE_AUTH_CLIENT_SECRET="integration_client_secret", # noqa: S106 GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", ): # Test flow creation doesn't raise errors @@ -301,8 +304,8 @@ def test_full_authentication_flow(self): # Test authentication with valid token auth = GoogleAuth( user=member, - access_token="integration_token", - refresh_token="integration_refresh", + access_token="integration_token", # noqa: S106 + refresh_token="integration_refresh", # noqa: S106 expires_at=timezone.now() + timedelta(hours=1), ) From 0f3b823ed04373bb25d9bee5a717fa18eecbbcb4 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 9 Aug 2025 19:40:22 +0300 Subject: [PATCH 05/25] Skip sonar and apply rabbit suggestion --- backend/apps/slack/models/google_auth.py | 4 +++- backend/tests/apps/slack/models/google_auth_test.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/apps/slack/models/google_auth.py b/backend/apps/slack/models/google_auth.py index 7d8e02984d..5d33b9e6e8 100644 --- a/backend/apps/slack/models/google_auth.py +++ b/backend/apps/slack/models/google_auth.py @@ -76,7 +76,9 @@ def authenticate(auth_url, user): @property def is_token_expired(self): """Check if the access token is expired.""" - return self.expires_at is None or self.expires_at <= timezone.now() + return self.expires_at is None or self.expires_at <= timezone.now() + timezone.timedelta( + seconds=60 + ) @staticmethod def refresh_access_token(auth): diff --git a/backend/tests/apps/slack/models/google_auth_test.py b/backend/tests/apps/slack/models/google_auth_test.py index fdf4f0d0cb..229e41bdd6 100644 --- a/backend/tests/apps/slack/models/google_auth_test.py +++ b/backend/tests/apps/slack/models/google_auth_test.py @@ -196,7 +196,7 @@ def test_authenticate_first_time(self, mock_get_or_create, mock_save, mock_get_f mock_get_or_create.assert_called_once_with(user=self.member) mock_flow_instance.fetch_token.assert_called_once_with( - authorization_response="http://auth.url", + authorization_response="http://auth.url", # NOSONAR ) mock_save.assert_called_once() From 4edeced64b9320ad3645742acee1071a01e53a08 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 9 Aug 2025 19:56:43 +0300 Subject: [PATCH 06/25] Update poetry lock --- backend/poetry.lock | 193 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 169 insertions(+), 24 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index ef595da017..532ab0f177 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -6,6 +6,7 @@ version = "2.6.1" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, @@ -17,6 +18,7 @@ version = "3.12.15" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc"}, {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af"}, @@ -116,7 +118,7 @@ propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns (>=3.3.0)", "brotlicffi"] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""] [[package]] name = "aiosignal" @@ -124,6 +126,7 @@ version = "1.4.0" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, @@ -138,6 +141,7 @@ version = "4.24.0" description = "A fully-featured and blazing-fast Python API client to interact with Algolia." optional = false python-versions = ">=3.8.1" +groups = ["main"] files = [ {file = "algoliasearch-4.24.0-py3-none-any.whl", hash = "sha256:223d188c2385895d6d49779ad3cd79e549963629141087cc8a38a9a7212b6ccf"}, {file = "algoliasearch-4.24.0.tar.gz", hash = "sha256:82854b633bc604e1b84073dfaa773d62da868dcf361e019ac87a294fc1dd4746"}, @@ -157,6 +161,7 @@ version = "4.0.0" description = "Algolia Search integration for Django" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "algoliasearch_django-4.0.0-py2.py3-none-any.whl", hash = "sha256:d160b86cd999607e9b3b0773a712e196e251af2b7dcb2480e40ef09440f3c80a"}, {file = "algoliasearch_django-4.0.0.tar.gz", hash = "sha256:c0acb8231163c16757d9e4c37a0ce882b89c4640a6dc836daaf479fd73c427b5"}, @@ -171,6 +176,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -182,6 +188,7 @@ version = "4.10.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, @@ -200,6 +207,7 @@ version = "3.9.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c"}, {file = "asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142"}, @@ -214,6 +222,7 @@ version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -225,18 +234,19 @@ version = "25.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "boto3" @@ -244,6 +254,7 @@ version = "1.40.5" description = "The AWS SDK for Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "boto3-1.40.5-py3-none-any.whl", hash = "sha256:8072f11a973709b582ef9834794721d543869cc506d029477172054dcc19c2da"}, {file = "boto3-1.40.5.tar.gz", hash = "sha256:7340706beffe93e3638adcd77cb266f46ba424f77623a5a6be591baa3cdbd3f3"}, @@ -263,6 +274,7 @@ version = "1.40.5" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "botocore-1.40.5-py3-none-any.whl", hash = "sha256:fbad8ce6605d45d12b9efbdcc9bbf68bcc5acc1ae3eb7dabb5919f38feeeeef7"}, {file = "botocore-1.40.5.tar.gz", hash = "sha256:f0c95e35a3a96a2b33d257160cbee923ae87b14ac3ba10f010f3baf8e03f73a2"}, @@ -282,6 +294,7 @@ version = "5.5.2" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, @@ -293,6 +306,7 @@ version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, @@ -304,6 +318,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -383,6 +398,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -394,6 +410,7 @@ version = "3.4.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, @@ -495,6 +512,7 @@ version = "8.2.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" +groups = ["dev"] files = [ {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, @@ -509,10 +527,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev", "test"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""} [[package]] name = "coverage" @@ -520,6 +540,7 @@ version = "7.10.2" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "coverage-7.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65"}, {file = "coverage-7.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8"}, @@ -612,7 +633,7 @@ files = [ ] [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" @@ -620,6 +641,7 @@ version = "45.0.6" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" +groups = ["main"] files = [ {file = "cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74"}, {file = "cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f"}, @@ -664,10 +686,10 @@ files = [ cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs ; python_full_version >= \"3.8.0\"", "sphinx-rtd-theme (>=3.0.0) ; python_full_version >= \"3.8.0\""] docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8.0\""] +pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==45.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] @@ -679,6 +701,7 @@ version = "1.15.4" description = "CSS unobfuscator and beautifier." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "cssbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:78c84d5e5378df7d08622bbd0477a1abdbd209680e95480bf22f12d5701efc98"}, {file = "cssbeautifier-1.15.4.tar.gz", hash = "sha256:9bb08dc3f64c101a01677f128acf01905914cf406baf87434dcde05b74c0acf5"}, @@ -695,6 +718,7 @@ version = "0.6.7" description = "Easily serialize dataclasses to and from JSON." optional = false python-versions = "<4.0,>=3.7" +groups = ["main"] files = [ {file = "dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a"}, {file = "dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0"}, @@ -710,6 +734,7 @@ version = "0.4.0" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, @@ -721,6 +746,7 @@ version = "1.9.0" description = "Distro - an OS platform information API" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, @@ -732,6 +758,7 @@ version = "5.2.5" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "django-5.2.5-py3-none-any.whl", hash = "sha256:2b2ada0ee8a5ff743a40e2b9820d1f8e24c11bac9ae6469cd548f0057ea6ddcd"}, {file = "django-5.2.5.tar.gz", hash = "sha256:0745b25681b129a77aae3d4f6549b62d3913d74407831abaa0d9021a03954bae"}, @@ -752,6 +779,7 @@ version = "2.5.1" description = "A helper for organizing Django settings." optional = false python-versions = "<4.0,>=3.8" +groups = ["main"] files = [ {file = "django-configurations-2.5.1.tar.gz", hash = "sha256:6e5083757e2bbdf9bb7850567536b96a93515f6b17503d74928ff628db2e0e94"}, {file = "django_configurations-2.5.1-py3-none-any.whl", hash = "sha256:ceb84858da2dac846b15e715c2fd936cfc4c7917c074aff8d31700564093955e"}, @@ -773,6 +801,7 @@ version = "4.7.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "django_cors_headers-4.7.0-py3-none-any.whl", hash = "sha256:f1c125dcd58479fe7a67fe2499c16ee38b81b397463cf025f0e2c42937421070"}, {file = "django_cors_headers-4.7.0.tar.gz", hash = "sha256:6fdf31bf9c6d6448ba09ef57157db2268d515d94fc5c89a0a1028e1fc03ee52b"}, @@ -788,6 +817,7 @@ version = "1.4.3" description = "Django Ninja - Fast Django REST framework" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "django_ninja-1.4.3-py3-none-any.whl", hash = "sha256:f3204137a059437b95677049474220611f1cf9efedba9213556474b75168fa01"}, {file = "django_ninja-1.4.3.tar.gz", hash = "sha256:e46d477ca60c228d2a5eb3cc912094928ea830d364501f966661eeada67cb038"}, @@ -808,6 +838,7 @@ version = "5.4.0" description = "Full featured redis cache backend for Django." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "django-redis-5.4.0.tar.gz", hash = "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42"}, {file = "django_redis-5.4.0-py3-none-any.whl", hash = "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b"}, @@ -826,6 +857,7 @@ version = "1.14.6" description = "Support for many storage backends in Django" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "django_storages-1.14.6-py3-none-any.whl", hash = "sha256:11b7b6200e1cb5ffcd9962bd3673a39c7d6a6109e8096f0e03d46fab3d3aabd9"}, {file = "django_storages-1.14.6.tar.gz", hash = "sha256:7a25ce8f4214f69ac9c7ce87e2603887f7ae99326c316bc8d2d75375e09341c9"}, @@ -850,6 +882,7 @@ version = "1.36.4" description = "HTML Template Linter and Formatter" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "djlint-1.36.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2dfb60883ceb92465201bfd392291a7597c6752baede6fbb6f1980cac8d6c5c"}, {file = "djlint-1.36.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc6a1320c0030244b530ac200642f883d3daa451a115920ef3d56d08b644292"}, @@ -892,6 +925,7 @@ version = "0.17.1" description = "EditorConfig File Locator and Interpreter for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "editorconfig-0.17.1-py3-none-any.whl", hash = "sha256:1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82"}, {file = "editorconfig-0.17.1.tar.gz", hash = "sha256:23c08b00e8e08cc3adcddb825251c497478df1dada6aefeb01e626ad37303745"}, @@ -906,6 +940,7 @@ version = "2.14.1" description = "Emoji for Python" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "emoji-2.14.1-py3-none-any.whl", hash = "sha256:35a8a486c1460addb1499e3bf7929d3889b2e2841a57401903699fef595e942b"}, {file = "emoji-2.14.1.tar.gz", hash = "sha256:f8c50043d79a2c1410ebfae833ae1868d5941a67a6cd4d18377e2eb0bd79346b"}, @@ -920,6 +955,7 @@ version = "2.1.1" description = "execnet: rapid multi-Python deployment" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, @@ -934,6 +970,7 @@ version = "3.18.0" description = "A platform independent file lock." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, @@ -942,7 +979,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] -typing = ["typing-extensions (>=4.12.2)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "frozenlist" @@ -950,6 +987,7 @@ version = "1.7.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a"}, {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61"}, @@ -1063,6 +1101,7 @@ version = "2.0" description = "The geodesic routines from GeographicLib" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "geographiclib-2.0-py3-none-any.whl", hash = "sha256:6b7225248e45ff7edcee32becc4e0a1504c606ac5ee163a5656d482e0cd38734"}, {file = "geographiclib-2.0.tar.gz", hash = "sha256:f7f41c85dc3e1c2d3d935ec86660dc3b2c848c83e17f9a9e51ba9d5146a15859"}, @@ -1074,6 +1113,7 @@ version = "2.4.1" description = "Python Geocoding Toolbox" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "geopy-2.4.1-py3-none-any.whl", hash = "sha256:ae8b4bc5c1131820f4d75fce9d4aaaca0c85189b3aa5d64c3dcaf5e3b7b882a7"}, {file = "geopy-2.4.1.tar.gz", hash = "sha256:50283d8e7ad07d89be5cb027338c6365a32044df3ae2556ad3f52f4840b3d0d1"}, @@ -1097,6 +1137,7 @@ version = "2.25.1" description = "Google API client core library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7"}, {file = "google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8"}, @@ -1111,7 +1152,7 @@ requests = ">=2.18.0,<3.0.0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.0)"] -grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0)", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0)"] +grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0) ; python_version >= \"3.11\""] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] @@ -1121,6 +1162,7 @@ version = "2.178.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google_api_python_client-2.178.0-py3-none-any.whl", hash = "sha256:f420adcd050150ff1baefa817e96e1ffa16872744f53471cd34096612e580c34"}, {file = "google_api_python_client-2.178.0.tar.gz", hash = "sha256:99cba921eb471bb5973b780c653ac54d96eef8a42f1b7375b7ab98f257a4414c"}, @@ -1139,6 +1181,7 @@ version = "2.40.3" description = "Google Authentication Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca"}, {file = "google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77"}, @@ -1152,11 +1195,11 @@ rsa = ">=3.1.4,<5" [package.extras] aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] enterprise-cert = ["cryptography", "pyopenssl"] -pyjwt = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] -pyopenssl = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +pyjwt = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] requests = ["requests (>=2.20.0,<3.0.0)"] -testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0)", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] +testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] urllib3 = ["packaging", "urllib3"] [[package]] @@ -1165,6 +1208,7 @@ version = "0.2.0" description = "Google Authentication Library: httplib2 transport" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, @@ -1180,6 +1224,7 @@ version = "1.2.2" description = "Google Authentication Library" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "google_auth_oauthlib-1.2.2-py3-none-any.whl", hash = "sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2"}, {file = "google_auth_oauthlib-1.2.2.tar.gz", hash = "sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684"}, @@ -1198,6 +1243,7 @@ version = "1.70.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"}, {file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"}, @@ -1215,6 +1261,7 @@ version = "3.2.6" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." optional = false python-versions = "<4,>=3.6" +groups = ["main"] files = [ {file = "graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f"}, {file = "graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab"}, @@ -1226,6 +1273,8 @@ version = "3.2.4" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "python_version == \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" files = [ {file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"}, {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"}, @@ -1293,6 +1342,7 @@ version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, @@ -1314,6 +1364,7 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -1325,6 +1376,7 @@ version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -1346,6 +1398,7 @@ version = "0.22.0" description = "A comprehensive HTTP client library." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] files = [ {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, @@ -1360,6 +1413,7 @@ version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -1372,7 +1426,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1384,6 +1438,7 @@ version = "0.4.1" description = "Consume Server-Sent Event (SSE) messages with HTTPX." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37"}, {file = "httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e"}, @@ -1395,6 +1450,7 @@ version = "4.12.3" description = "Python humanize utilities" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "humanize-4.12.3-py3-none-any.whl", hash = "sha256:2cbf6370af06568fa6d2da77c86edb7886f3160ecd19ee1ffef07979efc597f6"}, {file = "humanize-4.12.3.tar.gz", hash = "sha256:8430be3a615106fdfceb0b2c1b41c4c98c6b0fc5cc59663a5539b111dd325fb0"}, @@ -1409,6 +1465,7 @@ version = "2.6.12" description = "File identification library for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2"}, {file = "identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6"}, @@ -1423,6 +1480,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -1437,6 +1495,7 @@ version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -1448,6 +1507,7 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -1465,6 +1525,7 @@ version = "0.10.0" description = "Fast iterable JSON parser." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303"}, {file = "jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e"}, @@ -1551,6 +1612,7 @@ version = "1.0.1" description = "JSON Matching Expressions" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, @@ -1562,6 +1624,7 @@ version = "1.15.4" description = "JavaScript unobfuscator and beautifier." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "jsbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:72f65de312a3f10900d7685557f84cb61a9733c50dcc27271a39f5b0051bf528"}, {file = "jsbeautifier-1.15.4.tar.gz", hash = "sha256:5bb18d9efb9331d825735fbc5360ee8f1aac5e52780042803943aa7f854f7592"}, @@ -1577,13 +1640,14 @@ version = "0.12.0" description = "A Python implementation of the JSON5 data format." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db"}, {file = "json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a"}, ] [package.extras] -dev = ["build (==1.2.2.post1)", "coverage (==7.5.4)", "coverage (==7.8.0)", "mypy (==1.14.1)", "mypy (==1.15.0)", "pip (==25.0.1)", "pylint (==3.2.7)", "pylint (==3.3.6)", "ruff (==0.11.2)", "twine (==6.1.0)", "uv (==0.6.11)"] +dev = ["build (==1.2.2.post1)", "coverage (==7.5.4) ; python_version < \"3.9\"", "coverage (==7.8.0) ; python_version >= \"3.9\"", "mypy (==1.14.1) ; python_version < \"3.9\"", "mypy (==1.15.0) ; python_version >= \"3.9\"", "pip (==25.0.1)", "pylint (==3.2.7) ; python_version < \"3.9\"", "pylint (==3.3.6) ; python_version >= \"3.9\"", "ruff (==0.11.2)", "twine (==6.1.0)", "uv (==0.6.11)"] [[package]] name = "jsonpatch" @@ -1591,6 +1655,7 @@ version = "1.33" description = "Apply JSON-Patches (RFC 6902)" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +groups = ["main"] files = [ {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, @@ -1605,6 +1670,7 @@ version = "3.0.0" description = "Identify specific nodes in a JSON document (RFC 6901)" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, @@ -1616,6 +1682,7 @@ version = "0.3.27" description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.9" +groups = ["main"] files = [ {file = "langchain-0.3.27-py3-none-any.whl", hash = "sha256:7b20c4f338826acb148d885b20a73a16e410ede9ee4f19bb02011852d5f98798"}, {file = "langchain-0.3.27.tar.gz", hash = "sha256:aa6f1e6274ff055d0fd36254176770f356ed0a8994297d1df47df341953cec62"}, @@ -1655,6 +1722,7 @@ version = "0.3.27" description = "Community contributed LangChain integrations." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "langchain_community-0.3.27-py3-none-any.whl", hash = "sha256:581f97b795f9633da738ea95da9cb78f8879b538090c9b7a68c0aed49c828f0d"}, {file = "langchain_community-0.3.27.tar.gz", hash = "sha256:e1037c3b9da0c6d10bf06e838b034eb741e016515c79ef8f3f16e53ead33d882"}, @@ -1680,6 +1748,7 @@ version = "0.3.74" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "langchain_core-0.3.74-py3-none-any.whl", hash = "sha256:088338b5bc2f6a66892f9afc777992c24ee3188f41cbc603d09181e34a228ce7"}, {file = "langchain_core-0.3.74.tar.gz", hash = "sha256:ff604441aeade942fbcc0a3860a592daba7671345230c2078ba2eb5f82b6ba76"}, @@ -1700,6 +1769,7 @@ version = "0.3.9" description = "LangChain text splitting utilities" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "langchain_text_splitters-0.3.9-py3-none-any.whl", hash = "sha256:cee0bb816211584ea79cc79927317c358543f40404bcfdd69e69ba3ccde54401"}, {file = "langchain_text_splitters-0.3.9.tar.gz", hash = "sha256:7cd1e5a3aaf609979583eeca2eb34177622570b8fa8f586a605c6b1c34e7ebdb"}, @@ -1714,6 +1784,7 @@ version = "0.4.13" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "langsmith-0.4.13-py3-none-any.whl", hash = "sha256:dab7b16ee16986995007bf5a777f45c18f8bf7453f67ae2ebcb46ce43c214297"}, {file = "langsmith-0.4.13.tar.gz", hash = "sha256:1ae7dbb5d8150647406f49885a2dd16ab12bd990254b5dc23718838b3d086fde"}, @@ -1741,6 +1812,7 @@ version = "5.4.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"}, {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"}, @@ -1889,6 +1961,7 @@ version = "3.8.2" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24"}, {file = "markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45"}, @@ -1904,6 +1977,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -1974,6 +2048,7 @@ version = "3.26.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c"}, {file = "marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"}, @@ -1993,6 +2068,7 @@ version = "6.6.3" description = "multidict implementation" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817"}, {file = "multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140"}, @@ -2112,6 +2188,7 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -2123,6 +2200,7 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -2134,6 +2212,7 @@ version = "2.3.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.11" +groups = ["main"] files = [ {file = "numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9"}, {file = "numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168"}, @@ -2217,6 +2296,7 @@ version = "3.3.1" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, @@ -2233,6 +2313,7 @@ version = "1.99.4" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "openai-1.99.4-py3-none-any.whl", hash = "sha256:5a26181011252de3510d3c2dfdfaa97a08bb89ab700c1d054371a9df078a1fd2"}, {file = "openai-1.99.4.tar.gz", hash = "sha256:d177e6bd98dbce5a26ec584fbe6e91568a5b8b6f422f0ec7a4871adcaa9e3c51"}, @@ -2260,6 +2341,8 @@ version = "3.11.1" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "orjson-3.11.1-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:92d771c492b64119456afb50f2dff3e03a2db8b5af0eba32c5932d306f970532"}, {file = "orjson-3.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0085ef83a4141c2ed23bfec5fecbfdb1e95dd42fc8e8c76057bdeeec1608ea65"}, @@ -2352,6 +2435,7 @@ version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -2363,6 +2447,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -2374,6 +2459,7 @@ version = "0.4.1" description = "pgvector support for Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pgvector-0.4.1-py3-none-any.whl", hash = "sha256:34bb4e99e1b13d08a2fe82dda9f860f15ddcd0166fbb25bffe15821cbfeb7362"}, {file = "pgvector-0.4.1.tar.gz", hash = "sha256:83d3a1c044ff0c2f1e95d13dfb625beb0b65506cfec0941bfe81fd0ad44f4003"}, @@ -2388,6 +2474,7 @@ version = "11.3.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, @@ -2503,7 +2590,7 @@ fpx = ["olefile"] mic = ["olefile"] test-arrow = ["pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions"] +typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] @@ -2512,6 +2599,7 @@ version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, @@ -2528,6 +2616,7 @@ version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -2543,6 +2632,7 @@ version = "4.2.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, @@ -2561,6 +2651,7 @@ version = "0.3.2" description = "Accelerated property cache" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770"}, {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3"}, @@ -2668,6 +2759,7 @@ version = "1.26.1" description = "Beautiful, Pythonic protocol buffers" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, @@ -2685,6 +2777,7 @@ version = "6.31.1" description = "" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9"}, {file = "protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447"}, @@ -2703,6 +2796,7 @@ version = "2.9.10" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, @@ -2780,6 +2874,7 @@ version = "0.6.1" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, @@ -2791,6 +2886,7 @@ version = "0.4.2" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, @@ -2805,6 +2901,7 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -2816,6 +2913,7 @@ version = "2.11.7" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, @@ -2829,7 +2927,7 @@ typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -2837,6 +2935,7 @@ version = "2.33.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, @@ -2948,6 +3047,7 @@ version = "2.10.1" description = "Settings management using Pydantic" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796"}, {file = "pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee"}, @@ -2971,6 +3071,7 @@ version = "2.7.0" description = "Use the full Github API v3" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pygithub-2.7.0-py3-none-any.whl", hash = "sha256:40ecbfe26dc55cc34ab4b0ffa1d455e6f816ef9a2bc8d6f5ad18ce572f163700"}, {file = "pygithub-2.7.0.tar.gz", hash = "sha256:7cd6eafabb09b5369afba3586d86b1f1ad6f1326d2ff01bc47bb26615dce4cbb"}, @@ -2989,6 +3090,7 @@ version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -3003,6 +3105,7 @@ version = "2.10.1" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, @@ -3023,6 +3126,7 @@ version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, @@ -3049,6 +3153,7 @@ version = "3.2.3" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, @@ -3063,6 +3168,7 @@ version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, @@ -3084,6 +3190,7 @@ version = "6.2.1" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, @@ -3103,6 +3210,7 @@ version = "4.11.1" description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10"}, {file = "pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991"}, @@ -3121,6 +3229,7 @@ version = "3.14.1" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, @@ -3138,6 +3247,7 @@ version = "3.8.0" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, @@ -3158,6 +3268,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -3172,6 +3283,7 @@ version = "1.1.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.9" +groups = ["main", "test"] files = [ {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, @@ -3186,6 +3298,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -3248,6 +3361,7 @@ version = "3.13.0" description = "rapid fuzzy string matching" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "rapidfuzz-3.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aafc42a1dc5e1beeba52cd83baa41372228d6d8266f6d803c16dbabbcc156255"}, {file = "rapidfuzz-3.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:85c9a131a44a95f9cac2eb6e65531db014e09d89c4f18c7b1fa54979cb9ff1f3"}, @@ -3354,6 +3468,7 @@ version = "6.4.0" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f"}, {file = "redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010"}, @@ -3370,6 +3485,7 @@ version = "2025.7.34" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "regex-2025.7.34-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d856164d25e2b3b07b779bfed813eb4b6b6ce73c2fd818d46f47c1eb5cd79bd6"}, {file = "regex-2025.7.34-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d15a9da5fad793e35fb7be74eec450d968e05d2e294f3e0e77ab03fa7234a83"}, @@ -3466,6 +3582,7 @@ version = "4.4.3" description = "The Reportlab Toolkit" optional = false python-versions = "<4,>=3.7" +groups = ["main"] files = [ {file = "reportlab-4.4.3-py3-none-any.whl", hash = "sha256:df905dc5ec5ddaae91fc9cb3371af863311271d555236410954961c5ee6ee1b5"}, {file = "reportlab-4.4.3.tar.gz", hash = "sha256:073b0975dab69536acd3251858e6b0524ed3e087e71f1d0d1895acb50acf9c7b"}, @@ -3488,6 +3605,7 @@ version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, @@ -3509,6 +3627,7 @@ version = "2.0.0" description = "OAuthlib authentication support for Requests." optional = false python-versions = ">=3.4" +groups = ["main"] files = [ {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, @@ -3527,6 +3646,7 @@ version = "1.0.0" description = "A utility belt for advanced users of python-requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] files = [ {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, @@ -3541,6 +3661,7 @@ version = "4.9.1" description = "Pure-Python RSA implementation" optional = false python-versions = "<4,>=3.6" +groups = ["main"] files = [ {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, @@ -3555,6 +3676,7 @@ version = "0.11.13" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46"}, {file = "ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48"}, @@ -3582,6 +3704,7 @@ version = "0.13.1" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724"}, {file = "s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf"}, @@ -3599,6 +3722,7 @@ version = "2.34.1" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "sentry_sdk-2.34.1-py2.py3-none-any.whl", hash = "sha256:b7a072e1cdc5abc48101d5146e1ae680fa81fe886d8d95aaa25a0b450c818d32"}, {file = "sentry_sdk-2.34.1.tar.gz", hash = "sha256:69274eb8c5c38562a544c3e9f68b5be0a43be4b697f5fd385bf98e4fbe672687"}, @@ -3656,6 +3780,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -3667,6 +3792,7 @@ version = "1.23.0" description = "The Bolt Framework for Python" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "slack_bolt-1.23.0-py2.py3-none-any.whl", hash = "sha256:6d6ae39d80c964c362505ae4e587eed2b26dbc3a9f0cb76af1150c30fb670488"}, {file = "slack_bolt-1.23.0.tar.gz", hash = "sha256:3d2c3eb13131407a94f925eb22b180d352c2d97b808303ef92b7a46d6508c843"}, @@ -3681,6 +3807,7 @@ version = "3.36.0" description = "The Slack API Platform SDK for Python" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "slack_sdk-3.36.0-py2.py3-none-any.whl", hash = "sha256:6c96887d7175fc1b0b2777b73bb65f39b5b8bee9bd8acfec071d64014f9e2d10"}, {file = "slack_sdk-3.36.0.tar.gz", hash = "sha256:8586022bdbdf9f8f8d32f394540436c53b1e7c8da9d21e1eab4560ba70cfcffa"}, @@ -3695,6 +3822,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -3706,6 +3834,7 @@ version = "2.0.42" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "SQLAlchemy-2.0.42-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ee065898359fdee83961aed5cf1fb4cfa913ba71b58b41e036001d90bebbf7a"}, {file = "SQLAlchemy-2.0.42-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56bc76d86216443daa2e27e6b04a9b96423f0b69b5d0c40c7f4b9a4cdf7d8d90"}, @@ -3801,6 +3930,7 @@ version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, @@ -3816,6 +3946,7 @@ version = "0.270.6" description = "A library for creating GraphQL APIs" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "strawberry_graphql-0.270.6-py3-none-any.whl", hash = "sha256:e51e5cba074a0c19267f729e2aaf8782dfda7932a8deb9c256e8eea7a88ea419"}, {file = "strawberry_graphql-0.270.6.tar.gz", hash = "sha256:1a41963ae0f421ae4f9ec078551422b114273597caf999a4cb4091b6955003ac"}, @@ -3840,7 +3971,7 @@ debug-server = ["libcst (>=0.4.7,<1.8.0)", "pygments (>=2.3,<3.0)", "python-mult django = ["Django (>=3.2)", "asgiref (>=3.2,<4.0)"] fastapi = ["fastapi (>=0.65.2)", "python-multipart (>=0.0.7)"] flask = ["flask (>=1.1)"] -litestar = ["litestar (>=2)"] +litestar = ["litestar (>=2) ; python_version >= \"3.10\" and python_version < \"4.0\""] opentelemetry = ["opentelemetry-api (<2)", "opentelemetry-sdk (<2)"] pydantic = ["pydantic (>1.6.1)"] pyinstrument = ["pyinstrument (>=4.0.0)"] @@ -3853,6 +3984,7 @@ version = "0.59.1" description = "Strawberry GraphQL Django extension" optional = false python-versions = "<4.0,>=3.9" +groups = ["main"] files = [ {file = "strawberry_graphql_django-0.59.1-py3-none-any.whl", hash = "sha256:c0d74f6f2f140d8683fd3ac7405731ed63342d3aa9294443efe2412e51379227"}, {file = "strawberry_graphql_django-0.59.1.tar.gz", hash = "sha256:b886f1371539962f286b43d273fd91505f943e5e6688f191d6113576b40070ad"}, @@ -3873,6 +4005,7 @@ version = "9.1.2" description = "Retry code until it succeeds" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"}, {file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"}, @@ -3888,6 +4021,7 @@ version = "0.22.1" description = "Fuzzy string matching in python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481"}, {file = "thefuzz-0.22.1.tar.gz", hash = "sha256:7138039a7ecf540da323792d8592ef9902b1d79eb78c147d4f20664de79f3680"}, @@ -3902,6 +4036,7 @@ version = "4.67.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, @@ -3923,6 +4058,7 @@ version = "4.14.1" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, @@ -3934,6 +4070,7 @@ version = "0.9.0" description = "Runtime inspection utilities for typing module." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, @@ -3949,6 +4086,7 @@ version = "0.4.1" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, @@ -3963,6 +4101,8 @@ version = "2025.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main"] +markers = "sys_platform == \"win32\"" files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, @@ -3974,6 +4114,7 @@ version = "4.2.0" description = "Implementation of RFC 6570 URI Templates" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686"}, {file = "uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e"}, @@ -3985,13 +4126,14 @@ version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -4002,6 +4144,7 @@ version = "20.33.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67"}, {file = "virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8"}, @@ -4014,7 +4157,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "yarl" @@ -4022,6 +4165,7 @@ version = "1.20.1" description = "Yet another URL library" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4"}, {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a"}, @@ -4140,6 +4284,7 @@ version = "0.23.0" description = "Zstandard bindings for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"}, {file = "zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"}, @@ -4247,6 +4392,6 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\ cffi = ["cffi (>=1.11)"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.13" content-hash = "d76497e2278f6e1f53fb1e985a20d52c3ada777d9d3573e8adff3d42ddf74192" From e9b6b24316006d1dc204f6e0e782b920c39e8d61 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Mon, 11 Aug 2025 07:50:49 +0300 Subject: [PATCH 07/25] Add migrations --- .../apps/slack/migrations/0019_googleauth.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 backend/apps/slack/migrations/0019_googleauth.py diff --git a/backend/apps/slack/migrations/0019_googleauth.py b/backend/apps/slack/migrations/0019_googleauth.py new file mode 100644 index 0000000000..b00ff61805 --- /dev/null +++ b/backend/apps/slack/migrations/0019_googleauth.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.4 on 2025-08-11 04:45 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("slack", "0018_conversation_sync_messages"), + ] + + operations = [ + migrations.CreateModel( + name="GoogleAuth", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("access_token", models.TextField(blank=True, verbose_name="Access Token")), + ("refresh_token", models.TextField(blank=True, verbose_name="Refresh Token")), + ( + "expires_at", + models.DateTimeField(blank=True, null=True, verbose_name="Token Expiry"), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="google_auth", + to="slack.member", + verbose_name="Slack Member", + ), + ), + ], + ), + ] From 9e24628bf7943746cc2ec2ef05a4dfa9ed33bb4d Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Mon, 11 Aug 2025 08:32:08 +0300 Subject: [PATCH 08/25] Change user to member --- .../apps/slack/migrations/0019_googleauth.py | 2 +- backend/apps/slack/models/google_auth.py | 10 ++-- .../apps/slack/models/google_auth_test.py | 48 ++++++++++--------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/backend/apps/slack/migrations/0019_googleauth.py b/backend/apps/slack/migrations/0019_googleauth.py index b00ff61805..65c0a0c9fe 100644 --- a/backend/apps/slack/migrations/0019_googleauth.py +++ b/backend/apps/slack/migrations/0019_googleauth.py @@ -26,7 +26,7 @@ class Migration(migrations.Migration): models.DateTimeField(blank=True, null=True, verbose_name="Token Expiry"), ), ( - "user", + "member", models.OneToOneField( on_delete=django.db.models.deletion.CASCADE, related_name="google_auth", diff --git a/backend/apps/slack/models/google_auth.py b/backend/apps/slack/models/google_auth.py index 5d33b9e6e8..dfee31750b 100644 --- a/backend/apps/slack/models/google_auth.py +++ b/backend/apps/slack/models/google_auth.py @@ -13,7 +13,7 @@ class GoogleAuth(models.Model): """Model to store Google OAuth tokens for Slack integration.""" - user = models.OneToOneField( + member = models.OneToOneField( "slack.Member", on_delete=models.CASCADE, related_name="google_auth", @@ -52,11 +52,11 @@ def get_flow(): ) @staticmethod - def authenticate(auth_url, user): - """Authenticate a user and return a GoogleAuth instance.""" + def authenticate(auth_url, member): + """Authenticate a member and return a GoogleAuth instance.""" if not settings.IS_GOOGLE_AUTH_ENABLED: raise ValueError(AUTH_ERROR_MESSAGE) - auth = GoogleAuth.objects.get_or_create(user=user)[0] + auth = GoogleAuth.objects.get_or_create(member=member)[0] if auth.access_token and not auth.is_token_expired: return auth if auth.access_token: @@ -104,4 +104,4 @@ def refresh_access_token(auth): def __str__(self): """Return a string representation of the GoogleAuth instance.""" - return f"GoogleAuth(user={self.user})" + return f"GoogleAuth(member={self.member})" diff --git a/backend/tests/apps/slack/models/google_auth_test.py b/backend/tests/apps/slack/models/google_auth_test.py index 229e41bdd6..5ecf8b0fa0 100644 --- a/backend/tests/apps/slack/models/google_auth_test.py +++ b/backend/tests/apps/slack/models/google_auth_test.py @@ -27,35 +27,35 @@ def setUp(self): def test_google_auth_creation(self): """Test GoogleAuth model creation.""" auth = GoogleAuth( - user=self.member, + member=self.member, access_token=self.valid_token, refresh_token=self.valid_refresh_token, expires_at=self.future_time, ) - assert auth.user == self.member + assert auth.member == self.member assert auth.access_token == self.valid_token assert auth.refresh_token == self.valid_refresh_token assert auth.expires_at == self.future_time def test_string_representation(self): """Test string representation of GoogleAuth.""" - auth = GoogleAuth(user=self.member, access_token=self.valid_token) + auth = GoogleAuth(member=self.member, access_token=self.valid_token) - expected = f"GoogleAuth(user={self.member})" + expected = f"GoogleAuth(member={self.member})" assert str(auth) == expected def test_one_to_one_relationship(self): """Test one-to-one relationship with Member.""" - auth = GoogleAuth(user=self.member, access_token=self.valid_token) + auth = GoogleAuth(member=self.member, access_token=self.valid_token) assert self.member.google_auth == auth - assert auth.user == self.member + assert auth.member == self.member def test_is_token_expired_with_future_expiry(self): """Test is_token_expired property with future expiry.""" auth = GoogleAuth( - user=self.member, access_token=self.valid_token, expires_at=self.future_time + member=self.member, access_token=self.valid_token, expires_at=self.future_time ) assert not auth.is_token_expired @@ -63,14 +63,14 @@ def test_is_token_expired_with_future_expiry(self): def test_is_token_expired_with_past_expiry(self): """Test is_token_expired property with past expiry.""" auth = GoogleAuth( - user=self.member, access_token=self.valid_token, expires_at=self.expired_time + member=self.member, access_token=self.valid_token, expires_at=self.expired_time ) assert auth.is_token_expired def test_is_token_expired_with_none_expiry(self): """Test is_token_expired property with None expiry.""" - auth = GoogleAuth(user=self.member, access_token=self.valid_token, expires_at=None) + auth = GoogleAuth(member=self.member, access_token=self.valid_token, expires_at=None) assert auth.is_token_expired @@ -119,7 +119,7 @@ def test_authenticate_existing_valid_token(self, mock_get_flow, mock_get_or_crea mock_get_flow.return_value = Mock(spec=Flow) mock_get_or_create.return_value = ( GoogleAuth( - user=self.member, + member=self.member, access_token=self.valid_token, refresh_token=self.valid_refresh_token, expires_at=self.future_time, @@ -131,7 +131,7 @@ def test_authenticate_existing_valid_token(self, mock_get_flow, mock_get_or_crea assert result.access_token == self.valid_token assert result.refresh_token == self.valid_refresh_token assert result.expires_at == self.future_time - mock_get_or_create.assert_called_once_with(user=self.member) + mock_get_or_create.assert_called_once_with(member=self.member) mock_save.assert_not_called() @override_settings( @@ -146,7 +146,7 @@ def test_authenticate_existing_expired_token(self, mock_get_or_create, mock_refr """Test authenticate with existing expired token.""" # Create existing auth with expired token existing_auth = GoogleAuth( - user=self.member, + member=self.member, access_token=self.valid_token, refresh_token=self.valid_refresh_token, expires_at=self.expired_time, @@ -156,7 +156,7 @@ def test_authenticate_existing_expired_token(self, mock_get_or_create, mock_refr GoogleAuth.authenticate("http://auth.url", self.member) # NOSONAR mock_refresh.assert_called_once_with(existing_auth) - mock_get_or_create.assert_called_once_with(user=self.member) + mock_get_or_create.assert_called_once_with(member=self.member) @override_settings( IS_GOOGLE_AUTH_ENABLED=True, @@ -180,7 +180,7 @@ def test_authenticate_first_time(self, mock_get_or_create, mock_save, mock_get_f mock_get_flow.return_value = mock_flow_instance mock_get_or_create.return_value = ( GoogleAuth( - user=self.member, + member=self.member, access_token="", refresh_token="", expires_at=None, @@ -193,7 +193,7 @@ def test_authenticate_first_time(self, mock_get_or_create, mock_save, mock_get_f assert result.access_token == "new_access_token" # noqa: S105 assert result.refresh_token == "new_refresh_token" # noqa: S105 assert result.expires_at == self.future_time - mock_get_or_create.assert_called_once_with(user=self.member) + mock_get_or_create.assert_called_once_with(member=self.member) mock_flow_instance.fetch_token.assert_called_once_with( authorization_response="http://auth.url", # NOSONAR @@ -204,7 +204,9 @@ def test_authenticate_first_time(self, mock_get_or_create, mock_save, mock_get_f def test_refresh_access_token_when_disabled(self): """Test refresh_access_token raises error when Google auth is disabled.""" auth = GoogleAuth( - user=self.member, access_token=self.valid_token, refresh_token=self.valid_refresh_token + member=self.member, + access_token=self.valid_token, + refresh_token=self.valid_refresh_token, ) with pytest.raises(ValueError, match="Google OAuth client ID"): @@ -222,7 +224,7 @@ def test_refresh_access_token_success(self, mock_save, mock_get_flow): """Test successful refresh_access_token.""" # Create auth with refresh token auth = GoogleAuth( - user=self.member, + member=self.member, access_token=self.valid_token, refresh_token=self.valid_refresh_token, expires_at=self.expired_time, @@ -253,9 +255,9 @@ def test_refresh_access_token_success(self, mock_save, mock_get_flow): def test_verbose_names(self): """Test model field verbose names.""" - auth = GoogleAuth(user=self.member, access_token=self.valid_token) + auth = GoogleAuth(member=self.member, access_token=self.valid_token) - assert auth._meta.get_field("user").verbose_name == "Slack Member" + assert auth._meta.get_field("member").verbose_name == "Slack Member" assert auth._meta.get_field("access_token").verbose_name == "Access Token" assert auth._meta.get_field("refresh_token").verbose_name == "Refresh Token" assert auth._meta.get_field("expires_at").verbose_name == "Token Expiry" @@ -263,7 +265,7 @@ def test_verbose_names(self): def test_refresh_token_blank_allowed(self): """Test that refresh_token can be blank.""" auth = GoogleAuth( - user=self.member, + member=self.member, access_token=self.valid_token, refresh_token="", # Blank is allowed ) @@ -273,7 +275,7 @@ def test_refresh_token_blank_allowed(self): def test_expires_at_null_allowed(self): """Test that expires_at can be null.""" auth = GoogleAuth( - user=self.member, + member=self.member, access_token=self.valid_token, expires_at=None, # Null is allowed ) @@ -303,12 +305,12 @@ def test_full_authentication_flow(self): # Test authentication with valid token auth = GoogleAuth( - user=member, + member=member, access_token="integration_token", # noqa: S106 refresh_token="integration_refresh", # noqa: S106 expires_at=timezone.now() + timedelta(hours=1), ) assert not auth.is_token_expired - assert str(auth) == f"GoogleAuth(user={member})" + assert str(auth) == f"GoogleAuth(member={member})" assert member.google_auth == auth From f57e542fd91abb760d940ac2fcd839d230613aa3 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Tue, 12 Aug 2025 19:36:33 +0300 Subject: [PATCH 09/25] Update env.example --- backend/.env.example | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index c6714d414b..fed95b89e2 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -10,6 +10,9 @@ DJANGO_DB_NAME=None DJANGO_DB_PASSWORD=None DJANGO_DB_PORT=None DJANGO_DB_USER=None +DJANGO_GOOGLE_AUTH_CLIENT_ID=None +DJANGO_GOOGLE_AUTH_CLIENT_SECRET=None +DJANGO_GOOGLE_AUTH_REDIRECT_URI=http://localhost:8000/integrations/slack/oauth2/callback/ DJANGO_OPEN_AI_SECRET_KEY=None DJANGO_PUBLIC_IP_ADDRESS="127.0.0.1" DJANGO_REDIS_HOST=None @@ -20,6 +23,3 @@ DJANGO_SENTRY_DSN=None DJANGO_SLACK_BOT_TOKEN=None DJANGO_SLACK_SIGNING_SECRET=None GITHUB_TOKEN=None -GOOGLE_AUTH_CLIENT_ID=None -GOOGLE_AUTH_CLIENT_SECRET=None -GOOGLE_AUTH_REDIRECT_URI=http://localhost:8000/integrations/slack/oauth2/callback/ From a77d901ee0dabc3916d6310001ae1dbb0cf44b75 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Tue, 12 Aug 2025 19:38:46 +0300 Subject: [PATCH 10/25] Update poetry.lock --- backend/poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index f699e1ab15..23032db098 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -4608,4 +4608,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "02e382f49577289d5136d1ada9b3b78e2875a342950368938bb1a4ce982590c8" +content-hash = "15d539107f7453916c3087a68580c91253805039767d09b098be6de5f886c81b" From 675d58fe1d58987aa08d8bbcdfb0aafd1be4d703 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Tue, 12 Aug 2025 20:37:16 +0300 Subject: [PATCH 11/25] Update tokens to binary field --- ..._alter_googleauth_access_token_and_more.py | 22 +++++++++++++++++++ backend/apps/slack/models/google_auth.py | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 backend/apps/slack/migrations/0020_alter_googleauth_access_token_and_more.py diff --git a/backend/apps/slack/migrations/0020_alter_googleauth_access_token_and_more.py b/backend/apps/slack/migrations/0020_alter_googleauth_access_token_and_more.py new file mode 100644 index 0000000000..dfa1cee5a9 --- /dev/null +++ b/backend/apps/slack/migrations/0020_alter_googleauth_access_token_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.4 on 2025-08-12 17:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("slack", "0019_googleauth"), + ] + + operations = [ + migrations.AlterField( + model_name="googleauth", + name="access_token", + field=models.BinaryField(blank=True, verbose_name="Access Token"), + ), + migrations.AlterField( + model_name="googleauth", + name="refresh_token", + field=models.BinaryField(blank=True, verbose_name="Refresh Token"), + ), + ] diff --git a/backend/apps/slack/models/google_auth.py b/backend/apps/slack/models/google_auth.py index dfee31750b..ec705bf514 100644 --- a/backend/apps/slack/models/google_auth.py +++ b/backend/apps/slack/models/google_auth.py @@ -19,11 +19,11 @@ class GoogleAuth(models.Model): related_name="google_auth", verbose_name="Slack Member", ) - access_token = models.TextField( + access_token = models.BinaryField( verbose_name="Access Token", blank=True, ) - refresh_token = models.TextField( + refresh_token = models.BinaryField( verbose_name="Refresh Token", blank=True, ) From e68f177f00c635cef97b4d8938f866002959c453 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 13 Aug 2025 18:56:16 +0300 Subject: [PATCH 12/25] Improve auth logic --- backend/apps/slack/models/google_auth.py | 76 +++++---- .../apps/slack/models/google_auth_test.py | 157 +++++++++--------- 2 files changed, 128 insertions(+), 105 deletions(-) diff --git a/backend/apps/slack/models/google_auth.py b/backend/apps/slack/models/google_auth.py index ec705bf514..5abddbcc8a 100644 --- a/backend/apps/slack/models/google_auth.py +++ b/backend/apps/slack/models/google_auth.py @@ -1,10 +1,13 @@ """Slack Google OAuth Authentication Model.""" from django.conf import settings +from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone from google_auth_oauthlib.flow import Flow +from apps.slack.models.member import Member + AUTH_ERROR_MESSAGE = ( "Google OAuth client ID, secret, and redirect URI must be set in environment variables." ) @@ -19,40 +22,15 @@ class GoogleAuth(models.Model): related_name="google_auth", verbose_name="Slack Member", ) - access_token = models.BinaryField( - verbose_name="Access Token", - blank=True, - ) - refresh_token = models.BinaryField( - verbose_name="Refresh Token", - blank=True, - ) + access_token = models.BinaryField(verbose_name="Access Token", null=True) + refresh_token = models.BinaryField(verbose_name="Refresh Token", null=True) expires_at = models.DateTimeField( verbose_name="Token Expiry", - blank=True, null=True, ) @staticmethod - def get_flow(): - """Create a Google OAuth flow instance.""" - if not settings.IS_GOOGLE_AUTH_ENABLED: - raise ValueError(AUTH_ERROR_MESSAGE) - return Flow.from_client_config( - client_config={ - "web": { - "client_id": settings.GOOGLE_AUTH_CLIENT_ID, - "client_secret": settings.GOOGLE_AUTH_CLIENT_SECRET, - "redirect_uris": [settings.GOOGLE_AUTH_REDIRECT_URI], - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - } - }, - scopes=["https://www.googleapis.com/auth/calendar.readonly"], - ) - - @staticmethod - def authenticate(auth_url, member): + def authenticate(member): """Authenticate a member and return a GoogleAuth instance.""" if not settings.IS_GOOGLE_AUTH_ENABLED: raise ValueError(AUTH_ERROR_MESSAGE) @@ -63,16 +41,54 @@ def authenticate(auth_url, member): # If the access token is present but expired, refresh it GoogleAuth.refresh_access_token(auth) return auth + # If no access token is present, redirect to Google OAuth + flow = GoogleAuth.get_flow() + flow.redirect_uri = settings.GOOGLE_AUTH_REDIRECT_URI + state = member.slack_user_id + return flow.authorization_url(state=state) + + @staticmethod + def authenticate_callback(auth_response, member_id): + """Authenticate a member and return a GoogleAuth instance.""" + if not settings.IS_GOOGLE_AUTH_ENABLED: + raise ValueError(AUTH_ERROR_MESSAGE) + + member = None + try: + member = Member.objects.get(slack_user_id=member_id) + except Member.DoesNotExist as e: + error_message = f"Member with Slack ID {member_id} does not exist." + raise ValidationError(error_message) from e + + auth = GoogleAuth.objects.get_or_create(member=member)[0] # This is the first time authentication, so we need to fetch a new token flow = GoogleAuth.get_flow() flow.redirect_uri = settings.GOOGLE_AUTH_REDIRECT_URI - flow.fetch_token(authorization_response=auth_url) + flow.fetch_token(authorization_response=auth_response) auth.access_token = flow.credentials.token auth.refresh_token = flow.credentials.refresh_token auth.expires_at = flow.credentials.expiry auth.save() return auth + @staticmethod + def get_flow(): + """Create a Google OAuth flow instance.""" + if not settings.IS_GOOGLE_AUTH_ENABLED: + raise ValueError(AUTH_ERROR_MESSAGE) + return Flow.from_client_config( + client_config={ + "web": { + "client_id": settings.GOOGLE_AUTH_CLIENT_ID, + "client_secret": settings.GOOGLE_AUTH_CLIENT_SECRET, + "redirect_uris": [settings.GOOGLE_AUTH_REDIRECT_URI], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + } + }, + scopes=["https://www.googleapis.com/auth/calendar.readonly"], + ) + @property def is_token_expired(self): """Check if the access token is expired.""" @@ -87,7 +103,7 @@ def refresh_access_token(auth): raise ValueError(AUTH_ERROR_MESSAGE) refresh_error = "Google OAuth refresh token is not set or expired." if not auth.refresh_token: - raise ValueError(refresh_error) + raise ValidationError(refresh_error) flow = GoogleAuth.get_flow() flow.fetch_token( diff --git a/backend/tests/apps/slack/models/google_auth_test.py b/backend/tests/apps/slack/models/google_auth_test.py index 5ecf8b0fa0..6ffefc9db9 100644 --- a/backend/tests/apps/slack/models/google_auth_test.py +++ b/backend/tests/apps/slack/models/google_auth_test.py @@ -4,6 +4,7 @@ from unittest.mock import Mock, patch import pytest +from django.core.exceptions import ValidationError from django.test import override_settings from django.utils import timezone from google_auth_oauthlib.flow import Flow @@ -19,8 +20,8 @@ class TestGoogleAuthModel: def setUp(self): """Set up test data.""" self.member = Member(slack_user_id="U123456789", username="testuser") - self.valid_token = "valid_access_token" # noqa: S105 - self.valid_refresh_token = "valid_refresh_token" # noqa: S105 + self.valid_token = 0x587584 + self.valid_refresh_token = 0x123456 self.expired_time = timezone.now() - timedelta(hours=1) self.future_time = timezone.now() + timedelta(hours=1) @@ -101,7 +102,7 @@ def test_get_flow_success(self, mock_flow): def test_authenticate_when_disabled(self): """Test authenticate raises error when Google auth is disabled.""" with pytest.raises(ValueError, match="Google OAuth client ID"): - GoogleAuth.authenticate("http://auth.url", self.member) # NOSONAR + GoogleAuth.authenticate(self.member) @override_settings( IS_GOOGLE_AUTH_ENABLED=True, @@ -126,7 +127,7 @@ def test_authenticate_existing_valid_token(self, mock_get_flow, mock_get_or_crea ), True, ) - result = GoogleAuth.authenticate("http://auth.url", self.member) # NOSONAR + result = GoogleAuth.authenticate(self.member) assert result.access_token == self.valid_token assert result.refresh_token == self.valid_refresh_token @@ -153,7 +154,7 @@ def test_authenticate_existing_expired_token(self, mock_get_or_create, mock_refr ) mock_get_or_create.return_value = (existing_auth, False) - GoogleAuth.authenticate("http://auth.url", self.member) # NOSONAR + GoogleAuth.authenticate(self.member) mock_refresh.assert_called_once_with(existing_auth) mock_get_or_create.assert_called_once_with(member=self.member) @@ -165,40 +166,28 @@ def test_authenticate_existing_expired_token(self, mock_get_or_create, mock_refr GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", ) @patch("apps.slack.models.google_auth.GoogleAuth.get_flow") - @patch("apps.slack.models.google_auth.GoogleAuth.save") @patch("apps.slack.models.google_auth.GoogleAuth.objects.get_or_create") - def test_authenticate_first_time(self, mock_get_or_create, mock_save, mock_get_flow): + def test_authenticate_first_time(self, mock_get_or_create, mock_get_flow): """Test authenticate for first time (no existing token).""" # Mock flow and credentials - mock_credentials = Mock() - mock_credentials.token = "new_access_token" # noqa: S105 # NOSONAR - mock_credentials.refresh_token = "new_refresh_token" # noqa: S105 - mock_credentials.expiry = self.future_time - mock_flow_instance = Mock() - mock_flow_instance.credentials = mock_credentials mock_get_flow.return_value = mock_flow_instance mock_get_or_create.return_value = ( GoogleAuth( member=self.member, - access_token="", - refresh_token="", + access_token=None, + refresh_token=None, expires_at=None, ), True, ) + GoogleAuth.authenticate(self.member) - result = GoogleAuth.authenticate("http://auth.url", self.member) # NOSONAR - - assert result.access_token == "new_access_token" # noqa: S105 - assert result.refresh_token == "new_refresh_token" # noqa: S105 - assert result.expires_at == self.future_time mock_get_or_create.assert_called_once_with(member=self.member) - mock_flow_instance.fetch_token.assert_called_once_with( - authorization_response="http://auth.url", # NOSONAR + mock_flow_instance.authorization_url.assert_called_once_with( + state=self.member.slack_user_id ) - mock_save.assert_called_once() @override_settings(IS_GOOGLE_AUTH_ENABLED=False) def test_refresh_access_token_when_disabled(self): @@ -232,8 +221,8 @@ def test_refresh_access_token_success(self, mock_save, mock_get_flow): # Mock flow and new credentials mock_credentials = Mock() - mock_credentials.token = "new_access_token" # noqa: S105 # NOSONAR - mock_credentials.refresh_token = "new_refresh_token" # noqa: S105 + mock_credentials.token = 0x25848 # NOSONAR + mock_credentials.refresh_token = 0x123456 mock_credentials.expiry = self.future_time mock_flow_instance = Mock() @@ -242,8 +231,8 @@ def test_refresh_access_token_success(self, mock_save, mock_get_flow): GoogleAuth.refresh_access_token(auth) - assert auth.access_token == "new_access_token" # noqa: S105 - assert auth.refresh_token == "new_refresh_token" # noqa: S105 + assert auth.access_token == 0x25848 + assert auth.refresh_token == 0x123456 assert auth.expires_at == self.future_time mock_flow_instance.fetch_token.assert_called_once_with( @@ -253,6 +242,25 @@ def test_refresh_access_token_success(self, mock_save, mock_get_flow): ) mock_save.assert_called_once() + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", + ) + def test_refresh_token_not_found(self): + """Test refresh_access_token raises error when no refresh token is present.""" + auth = GoogleAuth( + member=self.member, + access_token=self.valid_token, + refresh_token=None, + ) + + with pytest.raises( + ValidationError, match="Google OAuth refresh token is not set or expired." + ): + GoogleAuth.refresh_access_token(auth) + def test_verbose_names(self): """Test model field verbose names.""" auth = GoogleAuth(member=self.member, access_token=self.valid_token) @@ -262,55 +270,54 @@ def test_verbose_names(self): assert auth._meta.get_field("refresh_token").verbose_name == "Refresh Token" assert auth._meta.get_field("expires_at").verbose_name == "Token Expiry" - def test_refresh_token_blank_allowed(self): - """Test that refresh_token can be blank.""" - auth = GoogleAuth( - member=self.member, - access_token=self.valid_token, - refresh_token="", # Blank is allowed - ) - - assert auth.refresh_token == "" - - def test_expires_at_null_allowed(self): - """Test that expires_at can be null.""" - auth = GoogleAuth( - member=self.member, - access_token=self.valid_token, - expires_at=None, # Null is allowed - ) - - assert auth.expires_at is None + @override_settings(IS_GOOGLE_AUTH_ENABLED=False) + def test_authenticate_callback_google_auth_disabled(self): + """Test authenticate_callback raises error when Google auth is disabled.""" + with pytest.raises(ValueError, match="Google OAuth client ID"): + GoogleAuth.authenticate_callback(auth_response={}, member_id=4) + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", + ) + @patch("apps.slack.models.google_auth.GoogleAuth.get_flow") + @patch("apps.slack.models.google_auth.GoogleAuth.objects.get_or_create") + @patch("apps.slack.models.google_auth.GoogleAuth.save") + @patch("apps.slack.models.google_auth.Member.objects.get") + def test_authenticate_callback_success( + self, mock_member_get, mock_save, mock_get_or_create, mock_get_flow + ): + """Test successful authenticate_callback.""" + mock_credentials = Mock() + mock_credentials.token = 0x25848 # NOSONAR + mock_credentials.refresh_token = 0x123456 + mock_credentials.expiry = self.future_time -class TestGoogleAuthIntegration: - """Integration tests for GoogleAuth model.""" + mock_flow_instance = Mock(spec=Flow) + mock_flow_instance.credentials = mock_credentials + mock_member_get.return_value = self.member + mock_get_flow.return_value = mock_flow_instance + mock_get_or_create.return_value = (GoogleAuth(member=self.member), False) + result = GoogleAuth.authenticate_callback({}, member_id=self.member.id) - def test_full_authentication_flow(self): - """Test complete authentication flow.""" - member = Member(slack_user_id="U123456789", username="testuser") + assert result.access_token == 0x25848 + assert result.refresh_token == 0x123456 + assert result.expires_at == self.future_time + mock_get_or_create.assert_called_once_with(member=self.member) + mock_save.assert_called_once() + mock_flow_instance.fetch_token.assert_called_once_with(authorization_response={}) - # Test that we can create and use GoogleAuth - with override_settings( - IS_GOOGLE_AUTH_ENABLED=True, - GOOGLE_AUTH_CLIENT_ID="integration_client_id", - GOOGLE_AUTH_CLIENT_SECRET="integration_client_secret", # noqa: S106 - GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", - ): - # Test flow creation doesn't raise errors - with patch("apps.slack.models.google_auth.Flow.from_client_config") as mock_flow: - mock_flow.return_value = Mock(spec=Flow) - flow = GoogleAuth.get_flow() - assert flow is not None - - # Test authentication with valid token - auth = GoogleAuth( - member=member, - access_token="integration_token", # noqa: S106 - refresh_token="integration_refresh", # noqa: S106 - expires_at=timezone.now() + timedelta(hours=1), - ) - - assert not auth.is_token_expired - assert str(auth) == f"GoogleAuth(member={member})" - assert member.google_auth == auth + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", + ) + @patch("apps.slack.models.member.Member.objects.get") + def test_authenticate_callback_member_not_found(self, mock_member_get): + """Test authenticate_callback raises error when member is not found.""" + mock_member_get.side_effect = Member.DoesNotExist + with pytest.raises(ValidationError, match="Member with Slack ID 4 does not exist."): + GoogleAuth.authenticate_callback(auth_response={}, member_id=4) From 5bf0a7905eb779b438001c1d53a861f96667f26f Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 13 Aug 2025 19:02:33 +0300 Subject: [PATCH 13/25] Apply migrations --- ..._alter_googleauth_access_token_and_more.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 backend/apps/slack/migrations/0021_alter_googleauth_access_token_and_more.py diff --git a/backend/apps/slack/migrations/0021_alter_googleauth_access_token_and_more.py b/backend/apps/slack/migrations/0021_alter_googleauth_access_token_and_more.py new file mode 100644 index 0000000000..9880b4fe49 --- /dev/null +++ b/backend/apps/slack/migrations/0021_alter_googleauth_access_token_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.4 on 2025-08-13 16:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("slack", "0020_alter_googleauth_access_token_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="googleauth", + name="access_token", + field=models.BinaryField(null=True, verbose_name="Access Token"), + ), + migrations.AlterField( + model_name="googleauth", + name="expires_at", + field=models.DateTimeField(null=True, verbose_name="Token Expiry"), + ), + migrations.AlterField( + model_name="googleauth", + name="refresh_token", + field=models.BinaryField(null=True, verbose_name="Refresh Token"), + ), + ] From e7bb5cff22d9633c30a7e1e0906afa077191ae25 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 13 Aug 2025 23:25:07 +0300 Subject: [PATCH 14/25] Apply rabbit's suggestions --- backend/apps/slack/models/google_auth.py | 12 +++++++-- .../apps/slack/models/google_auth_test.py | 27 ++++++++++--------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/backend/apps/slack/models/google_auth.py b/backend/apps/slack/models/google_auth.py index 5abddbcc8a..953afe5a04 100644 --- a/backend/apps/slack/models/google_auth.py +++ b/backend/apps/slack/models/google_auth.py @@ -45,7 +45,12 @@ def authenticate(member): flow = GoogleAuth.get_flow() flow.redirect_uri = settings.GOOGLE_AUTH_REDIRECT_URI state = member.slack_user_id - return flow.authorization_url(state=state) + return flow.authorization_url( + access_type="offline", + include_granted_scopes="true", + prompt="consent", + state=state, + ) @staticmethod def authenticate_callback(auth_response, member_id): @@ -67,7 +72,10 @@ def authenticate_callback(auth_response, member_id): flow.fetch_token(authorization_response=auth_response) auth.access_token = flow.credentials.token auth.refresh_token = flow.credentials.refresh_token - auth.expires_at = flow.credentials.expiry + expires_at = flow.credentials.expiry + if expires_at and timezone.is_naive(expires_at): + expires_at = timezone.make_aware(expires_at) + auth.expires_at = expires_at auth.save() return auth diff --git a/backend/tests/apps/slack/models/google_auth_test.py b/backend/tests/apps/slack/models/google_auth_test.py index 6ffefc9db9..99d001473f 100644 --- a/backend/tests/apps/slack/models/google_auth_test.py +++ b/backend/tests/apps/slack/models/google_auth_test.py @@ -20,8 +20,8 @@ class TestGoogleAuthModel: def setUp(self): """Set up test data.""" self.member = Member(slack_user_id="U123456789", username="testuser") - self.valid_token = 0x587584 - self.valid_refresh_token = 0x123456 + self.valid_token = b"valid_token" + self.valid_refresh_token = b"valid_refresh_token" self.expired_time = timezone.now() - timedelta(hours=1) self.future_time = timezone.now() + timedelta(hours=1) @@ -186,7 +186,10 @@ def test_authenticate_first_time(self, mock_get_or_create, mock_get_flow): mock_get_or_create.assert_called_once_with(member=self.member) mock_flow_instance.authorization_url.assert_called_once_with( - state=self.member.slack_user_id + access_type="offline", + include_granted_scopes="true", + prompt="consent", + state=self.member.slack_user_id, ) @override_settings(IS_GOOGLE_AUTH_ENABLED=False) @@ -221,8 +224,8 @@ def test_refresh_access_token_success(self, mock_save, mock_get_flow): # Mock flow and new credentials mock_credentials = Mock() - mock_credentials.token = 0x25848 # NOSONAR - mock_credentials.refresh_token = 0x123456 + mock_credentials.token = b"token" # NOSONAR + mock_credentials.refresh_token = b"refresh_token" mock_credentials.expiry = self.future_time mock_flow_instance = Mock() @@ -231,8 +234,8 @@ def test_refresh_access_token_success(self, mock_save, mock_get_flow): GoogleAuth.refresh_access_token(auth) - assert auth.access_token == 0x25848 - assert auth.refresh_token == 0x123456 + assert auth.access_token == b"token" + assert auth.refresh_token == b"refresh_token" assert auth.expires_at == self.future_time mock_flow_instance.fetch_token.assert_called_once_with( @@ -291,8 +294,8 @@ def test_authenticate_callback_success( ): """Test successful authenticate_callback.""" mock_credentials = Mock() - mock_credentials.token = 0x25848 # NOSONAR - mock_credentials.refresh_token = 0x123456 + mock_credentials.token = b"token" # NOSONAR + mock_credentials.refresh_token = b"refresh_token" mock_credentials.expiry = self.future_time mock_flow_instance = Mock(spec=Flow) @@ -300,10 +303,10 @@ def test_authenticate_callback_success( mock_member_get.return_value = self.member mock_get_flow.return_value = mock_flow_instance mock_get_or_create.return_value = (GoogleAuth(member=self.member), False) - result = GoogleAuth.authenticate_callback({}, member_id=self.member.id) + result = GoogleAuth.authenticate_callback({}, member_id=self.member.slack_user_id) - assert result.access_token == 0x25848 - assert result.refresh_token == 0x123456 + assert result.access_token == b"token" + assert result.refresh_token == b"refresh_token" assert result.expires_at == self.future_time mock_get_or_create.assert_called_once_with(member=self.member) mock_save.assert_called_once() From 24ff1d0cf0e7251945591abf3f5c6247a69c0308 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 14 Aug 2025 02:21:01 +0300 Subject: [PATCH 15/25] Separate google client from slack.GoogleAuth model --- backend/apps/common/clients.py | 17 +++++++++++++++++ backend/apps/slack/models/google_auth.py | 15 ++------------- .../tests/apps/slack/models/google_auth_test.py | 17 ----------------- 3 files changed, 19 insertions(+), 30 deletions(-) create mode 100644 backend/apps/common/clients.py diff --git a/backend/apps/common/clients.py b/backend/apps/common/clients.py new file mode 100644 index 0000000000..b826473362 --- /dev/null +++ b/backend/apps/common/clients.py @@ -0,0 +1,17 @@ +"""Common API Clients.""" + +from django.conf import settings +from google_auth_oauthlib.flow import Flow + +google_auth_client = Flow.from_client_config( + client_config={ + "web": { + "client_id": settings.GOOGLE_AUTH_CLIENT_ID, + "client_secret": settings.GOOGLE_AUTH_CLIENT_SECRET, + "redirect_uris": [settings.GOOGLE_AUTH_REDIRECT_URI], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + } + }, + scopes=["https://www.googleapis.com/auth/calendar.readonly"], +) diff --git a/backend/apps/slack/models/google_auth.py b/backend/apps/slack/models/google_auth.py index 953afe5a04..acba1c0d06 100644 --- a/backend/apps/slack/models/google_auth.py +++ b/backend/apps/slack/models/google_auth.py @@ -4,8 +4,8 @@ from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone -from google_auth_oauthlib.flow import Flow +from apps.common.clients import google_auth_client from apps.slack.models.member import Member AUTH_ERROR_MESSAGE = ( @@ -84,18 +84,7 @@ def get_flow(): """Create a Google OAuth flow instance.""" if not settings.IS_GOOGLE_AUTH_ENABLED: raise ValueError(AUTH_ERROR_MESSAGE) - return Flow.from_client_config( - client_config={ - "web": { - "client_id": settings.GOOGLE_AUTH_CLIENT_ID, - "client_secret": settings.GOOGLE_AUTH_CLIENT_SECRET, - "redirect_uris": [settings.GOOGLE_AUTH_REDIRECT_URI], - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - } - }, - scopes=["https://www.googleapis.com/auth/calendar.readonly"], - ) + return google_auth_client @property def is_token_expired(self): diff --git a/backend/tests/apps/slack/models/google_auth_test.py b/backend/tests/apps/slack/models/google_auth_test.py index 99d001473f..86a5a6438b 100644 --- a/backend/tests/apps/slack/models/google_auth_test.py +++ b/backend/tests/apps/slack/models/google_auth_test.py @@ -81,23 +81,6 @@ def test_get_flow_when_disabled(self): with pytest.raises(ValueError, match="Google OAuth client ID"): GoogleAuth.get_flow() - @override_settings( - IS_GOOGLE_AUTH_ENABLED=True, - GOOGLE_AUTH_CLIENT_ID="test_client_id", - GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 - GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", - ) - @override_settings(IS_GOOGLE_AUTH_ENABLED=True) - @patch("apps.slack.models.google_auth.Flow.from_client_config") - def test_get_flow_success(self, mock_flow): - """Test successful get_flow creation.""" - mock_flow_instance = Mock(spec=Flow) - mock_flow.return_value = mock_flow_instance - - result = GoogleAuth.get_flow() - - assert result == mock_flow_instance - @override_settings(IS_GOOGLE_AUTH_ENABLED=False) def test_authenticate_when_disabled(self): """Test authenticate raises error when Google auth is disabled.""" From 58a6b29d2c8e9322453d5069592242657221fb1e Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 14 Aug 2025 02:40:58 +0300 Subject: [PATCH 16/25] Convert singleton to factory --- backend/apps/common/clients.py | 27 +++++++++++++----------- backend/apps/slack/models/google_auth.py | 4 ++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/backend/apps/common/clients.py b/backend/apps/common/clients.py index b826473362..45956be958 100644 --- a/backend/apps/common/clients.py +++ b/backend/apps/common/clients.py @@ -3,15 +3,18 @@ from django.conf import settings from google_auth_oauthlib.flow import Flow -google_auth_client = Flow.from_client_config( - client_config={ - "web": { - "client_id": settings.GOOGLE_AUTH_CLIENT_ID, - "client_secret": settings.GOOGLE_AUTH_CLIENT_SECRET, - "redirect_uris": [settings.GOOGLE_AUTH_REDIRECT_URI], - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - } - }, - scopes=["https://www.googleapis.com/auth/calendar.readonly"], -) + +def get_google_auth_client(): + """Get a Google OAuth client.""" + return Flow.from_client_config( + client_config={ + "web": { + "client_id": settings.GOOGLE_AUTH_CLIENT_ID, + "client_secret": settings.GOOGLE_AUTH_CLIENT_SECRET, + "redirect_uris": [settings.GOOGLE_AUTH_REDIRECT_URI], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + } + }, + scopes=["https://www.googleapis.com/auth/calendar.readonly"], + ) diff --git a/backend/apps/slack/models/google_auth.py b/backend/apps/slack/models/google_auth.py index acba1c0d06..ce47b980de 100644 --- a/backend/apps/slack/models/google_auth.py +++ b/backend/apps/slack/models/google_auth.py @@ -5,7 +5,7 @@ from django.db import models from django.utils import timezone -from apps.common.clients import google_auth_client +from apps.common.clients import get_google_auth_client from apps.slack.models.member import Member AUTH_ERROR_MESSAGE = ( @@ -84,7 +84,7 @@ def get_flow(): """Create a Google OAuth flow instance.""" if not settings.IS_GOOGLE_AUTH_ENABLED: raise ValueError(AUTH_ERROR_MESSAGE) - return google_auth_client + return get_google_auth_client() @property def is_token_expired(self): From 318f77196d076d6f401856c546789e224c82b398 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 14 Aug 2025 02:43:52 +0300 Subject: [PATCH 17/25] Update auth_uri --- backend/apps/common/clients.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/apps/common/clients.py b/backend/apps/common/clients.py index 45956be958..28c8994ab9 100644 --- a/backend/apps/common/clients.py +++ b/backend/apps/common/clients.py @@ -12,7 +12,7 @@ def get_google_auth_client(): "client_id": settings.GOOGLE_AUTH_CLIENT_ID, "client_secret": settings.GOOGLE_AUTH_CLIENT_SECRET, "redirect_uris": [settings.GOOGLE_AUTH_REDIRECT_URI], - "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth", "token_uri": "https://oauth2.googleapis.com/token", } }, From f22b9351476e3bfd919ca070859834b3532e1016 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 14 Aug 2025 02:57:14 +0300 Subject: [PATCH 18/25] Apply suggestions --- backend/apps/slack/models/google_auth.py | 9 +++++++-- backend/tests/apps/slack/models/google_auth_test.py | 1 - 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/apps/slack/models/google_auth.py b/backend/apps/slack/models/google_auth.py index ce47b980de..ca2ab8a440 100644 --- a/backend/apps/slack/models/google_auth.py +++ b/backend/apps/slack/models/google_auth.py @@ -31,7 +31,13 @@ class GoogleAuth(models.Model): @staticmethod def authenticate(member): - """Authenticate a member and return a GoogleAuth instance.""" + """Authenticate a member. + + Returns: + - GoogleAuth instance if a valid/refreshable token exists, or + - (authorization_url, state) tuple to complete the OAuth flow. + + """ if not settings.IS_GOOGLE_AUTH_ENABLED: raise ValueError(AUTH_ERROR_MESSAGE) auth = GoogleAuth.objects.get_or_create(member=member)[0] @@ -47,7 +53,6 @@ def authenticate(member): state = member.slack_user_id return flow.authorization_url( access_type="offline", - include_granted_scopes="true", prompt="consent", state=state, ) diff --git a/backend/tests/apps/slack/models/google_auth_test.py b/backend/tests/apps/slack/models/google_auth_test.py index 86a5a6438b..f90137219a 100644 --- a/backend/tests/apps/slack/models/google_auth_test.py +++ b/backend/tests/apps/slack/models/google_auth_test.py @@ -170,7 +170,6 @@ def test_authenticate_first_time(self, mock_get_or_create, mock_get_flow): mock_flow_instance.authorization_url.assert_called_once_with( access_type="offline", - include_granted_scopes="true", prompt="consent", state=self.member.slack_user_id, ) From b997b9e22b258cf6240699cf0bf73b48e7bb789c Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 16 Aug 2025 15:19:59 +0300 Subject: [PATCH 19/25] Add meta class --- ...ogleauth_options_alter_googleauth_table.py | 20 +++++++++++++++++++ backend/apps/slack/models/google_auth.py | 4 ++++ 2 files changed, 24 insertions(+) create mode 100644 backend/apps/slack/migrations/0022_alter_googleauth_options_alter_googleauth_table.py diff --git a/backend/apps/slack/migrations/0022_alter_googleauth_options_alter_googleauth_table.py b/backend/apps/slack/migrations/0022_alter_googleauth_options_alter_googleauth_table.py new file mode 100644 index 0000000000..02d75225c8 --- /dev/null +++ b/backend/apps/slack/migrations/0022_alter_googleauth_options_alter_googleauth_table.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.4 on 2025-08-16 12:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("slack", "0021_alter_googleauth_access_token_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="googleauth", + options={"verbose_name_plural": "Google Auths"}, + ), + migrations.AlterModelTable( + name="googleauth", + table="slack_google_auths", + ), + ] diff --git a/backend/apps/slack/models/google_auth.py b/backend/apps/slack/models/google_auth.py index ca2ab8a440..d1bd610ae6 100644 --- a/backend/apps/slack/models/google_auth.py +++ b/backend/apps/slack/models/google_auth.py @@ -16,6 +16,10 @@ class GoogleAuth(models.Model): """Model to store Google OAuth tokens for Slack integration.""" + class Meta: + db_table = "slack_google_auths" + verbose_name_plural = "Google Auths" + member = models.OneToOneField( "slack.Member", on_delete=models.CASCADE, From 0a1891723aeda707340eb59d7ffeff886a0868a0 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 16 Aug 2025 15:53:18 +0300 Subject: [PATCH 20/25] Update refresh logic and tests --- backend/.env.example | 2 ++ backend/apps/common/clients.py | 4 +-- backend/apps/slack/models/google_auth.py | 10 ++++--- backend/settings/base.py | 2 ++ .../apps/slack/models/google_auth_test.py | 27 ++++++++++--------- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index fed95b89e2..53d6c576f9 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -10,9 +10,11 @@ DJANGO_DB_NAME=None DJANGO_DB_PASSWORD=None DJANGO_DB_PORT=None DJANGO_DB_USER=None +DJANGO_GOOGLE_AUTH_AUTH_URI=https://accounts.google.com/o/oauth2/auth DJANGO_GOOGLE_AUTH_CLIENT_ID=None DJANGO_GOOGLE_AUTH_CLIENT_SECRET=None DJANGO_GOOGLE_AUTH_REDIRECT_URI=http://localhost:8000/integrations/slack/oauth2/callback/ +DJANGO_GOOGLE_AUTH_TOKEN_URI=https://oauth2.googleapis.com/token DJANGO_OPEN_AI_SECRET_KEY=None DJANGO_PUBLIC_IP_ADDRESS="127.0.0.1" DJANGO_REDIS_HOST=None diff --git a/backend/apps/common/clients.py b/backend/apps/common/clients.py index 28c8994ab9..f9f1e63396 100644 --- a/backend/apps/common/clients.py +++ b/backend/apps/common/clients.py @@ -12,8 +12,8 @@ def get_google_auth_client(): "client_id": settings.GOOGLE_AUTH_CLIENT_ID, "client_secret": settings.GOOGLE_AUTH_CLIENT_SECRET, "redirect_uris": [settings.GOOGLE_AUTH_REDIRECT_URI], - "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth", - "token_uri": "https://oauth2.googleapis.com/token", + "auth_uri": settings.GOOGLE_AUTH_AUTH_URI, + "token_uri": settings.GOOGLE_AUTH_TOKEN_URI, } }, scopes=["https://www.googleapis.com/auth/calendar.readonly"], diff --git a/backend/apps/slack/models/google_auth.py b/backend/apps/slack/models/google_auth.py index d1bd610ae6..d94eb52176 100644 --- a/backend/apps/slack/models/google_auth.py +++ b/backend/apps/slack/models/google_auth.py @@ -4,6 +4,8 @@ from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials from apps.common.clients import get_google_auth_client from apps.slack.models.member import Member @@ -110,15 +112,15 @@ def refresh_access_token(auth): refresh_error = "Google OAuth refresh token is not set or expired." if not auth.refresh_token: raise ValidationError(refresh_error) - - flow = GoogleAuth.get_flow() - flow.fetch_token( + credentials = Credentials( + token=auth.access_token, refresh_token=auth.refresh_token, + token_uri=settings.GOOGLE_AUTH_TOKEN_URI, client_id=settings.GOOGLE_AUTH_CLIENT_ID, client_secret=settings.GOOGLE_AUTH_CLIENT_SECRET, ) + credentials.refresh(Request()) - credentials = flow.credentials auth.access_token = credentials.token auth.refresh_token = credentials.refresh_token auth.expires_at = credentials.expiry diff --git a/backend/settings/base.py b/backend/settings/base.py index b945ab9cce..59f7e53eea 100644 --- a/backend/settings/base.py +++ b/backend/settings/base.py @@ -19,9 +19,11 @@ class Base(Configuration): DEBUG = False GITHUB_APP_ID = None GITHUB_APP_INSTALLATION_ID = None + GOOGLE_AUTH_AUTH_URI = values.Value(environ_name="GOOGLE_AUTH_AUTH_URI") GOOGLE_AUTH_CLIENT_ID = values.Value(environ_name="GOOGLE_AUTH_CLIENT_ID") GOOGLE_AUTH_CLIENT_SECRET = values.Value(environ_name="GOOGLE_AUTH_CLIENT_SECRET") GOOGLE_AUTH_REDIRECT_URI = values.Value(environ_name="GOOGLE_AUTH_REDIRECT_URI") + GOOGLE_AUTH_TOKEN_URI = values.Value(environ_name="GOOGLE_AUTH_TOKEN_URI") IS_GOOGLE_AUTH_ENABLED = all( value not in (None, "None", "") diff --git a/backend/tests/apps/slack/models/google_auth_test.py b/backend/tests/apps/slack/models/google_auth_test.py index f90137219a..e8d2a010ca 100644 --- a/backend/tests/apps/slack/models/google_auth_test.py +++ b/backend/tests/apps/slack/models/google_auth_test.py @@ -4,6 +4,7 @@ from unittest.mock import Mock, patch import pytest +from django.conf import settings from django.core.exceptions import ValidationError from django.test import override_settings from django.utils import timezone @@ -192,9 +193,10 @@ def test_refresh_access_token_when_disabled(self): GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", ) - @patch("apps.slack.models.google_auth.GoogleAuth.get_flow") + @patch("apps.slack.models.google_auth.Credentials") + @patch("apps.slack.models.google_auth.Request") @patch("apps.slack.models.google_auth.GoogleAuth.save") - def test_refresh_access_token_success(self, mock_save, mock_get_flow): + def test_refresh_access_token_success(self, mock_save, mock_request, mock_credentials): """Test successful refresh_access_token.""" # Create auth with refresh token auth = GoogleAuth( @@ -205,14 +207,12 @@ def test_refresh_access_token_success(self, mock_save, mock_get_flow): ) # Mock flow and new credentials - mock_credentials = Mock() - mock_credentials.token = b"token" # NOSONAR - mock_credentials.refresh_token = b"refresh_token" - mock_credentials.expiry = self.future_time + mock_credentials_instance = Mock() + mock_credentials_instance.token = b"token" # NOSONAR + mock_credentials_instance.refresh_token = b"refresh_token" + mock_credentials_instance.expiry = self.future_time - mock_flow_instance = Mock() - mock_flow_instance.credentials = mock_credentials - mock_get_flow.return_value = mock_flow_instance + mock_credentials.return_value = mock_credentials_instance GoogleAuth.refresh_access_token(auth) @@ -220,11 +220,14 @@ def test_refresh_access_token_success(self, mock_save, mock_get_flow): assert auth.refresh_token == b"refresh_token" assert auth.expires_at == self.future_time - mock_flow_instance.fetch_token.assert_called_once_with( + mock_credentials.assert_called_once_with( + token=self.valid_token, refresh_token=self.valid_refresh_token, - client_id="test_client_id", - client_secret="test_client_secret", # noqa: S106 + token_uri=settings.GOOGLE_AUTH_TOKEN_URI, + client_id=settings.GOOGLE_AUTH_CLIENT_ID, + client_secret=settings.GOOGLE_AUTH_CLIENT_SECRET, ) + mock_credentials_instance.refresh.assert_called_once_with(mock_request.return_value) mock_save.assert_called_once() @override_settings( From 54839efe9837dcebe4896e4c9b7f6f9ac1d84f28 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 16 Aug 2025 18:25:28 +0300 Subject: [PATCH 21/25] Make google auth credentials secret --- backend/settings/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/settings/base.py b/backend/settings/base.py index 59f7e53eea..da4f7c7a5c 100644 --- a/backend/settings/base.py +++ b/backend/settings/base.py @@ -20,8 +20,8 @@ class Base(Configuration): GITHUB_APP_ID = None GITHUB_APP_INSTALLATION_ID = None GOOGLE_AUTH_AUTH_URI = values.Value(environ_name="GOOGLE_AUTH_AUTH_URI") - GOOGLE_AUTH_CLIENT_ID = values.Value(environ_name="GOOGLE_AUTH_CLIENT_ID") - GOOGLE_AUTH_CLIENT_SECRET = values.Value(environ_name="GOOGLE_AUTH_CLIENT_SECRET") + GOOGLE_AUTH_CLIENT_ID = values.SecretValue(environ_name="GOOGLE_AUTH_CLIENT_ID") + GOOGLE_AUTH_CLIENT_SECRET = values.SecretValue(environ_name="GOOGLE_AUTH_CLIENT_SECRET") GOOGLE_AUTH_REDIRECT_URI = values.Value(environ_name="GOOGLE_AUTH_REDIRECT_URI") GOOGLE_AUTH_TOKEN_URI = values.Value(environ_name="GOOGLE_AUTH_TOKEN_URI") From 5820460717e50a86e606e4d931663430f3469742 Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Tue, 19 Aug 2025 19:24:50 -0700 Subject: [PATCH 22/25] Update code --- backend/pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 50edc38fe9..6f8c8e9807 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -22,6 +22,9 @@ django-redis = "^6.0.0" django-storages = { extras = [ "s3" ], version = "^1.14.4" } emoji = "^2.14.1" geopy = "^2.4.1" +google-api-python-client = "^2.178.0" +google-auth-httplib2 = "^0.2.0" +google-auth-oauthlib = "^1.2.2" gunicorn = "^23.0.0" humanize = "^4.11.0" jinja2 = "^3.1.6" @@ -47,9 +50,6 @@ slack-sdk = "^3.35.0" strawberry-graphql = { extras = [ "django" ], version = "^0.278.1" } strawberry-graphql-django = "^0.65.1" thefuzz = "^0.22.1" -google-api-python-client = "^2.178.0" -google-auth-httplib2 = "^0.2.0" -google-auth-oauthlib = "^1.2.2" [tool.poetry.group.dev.dependencies] djlint = "^1.36.4" From 943e9396ef439ee157a6303badbaa3a097ad2bd0 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 20 Aug 2025 21:56:12 +0300 Subject: [PATCH 23/25] Apply suggestions --- backend/.env.example | 4 +- backend/apps/common/clients.py | 2 +- .../0004_membergooglecredentials.py | 41 +++ ...embergooglecredentials_options_and_more.py | 20 ++ backend/apps/nest/models/__init__.py | 1 + .../models/member_google_credentials.py} | 24 +- .../migrations/0023_delete_googleauth.py | 15 + backend/apps/slack/models/__init__.py | 1 - backend/settings/base.py | 18 +- .../models/member_google_credentials_test.py | 325 ++++++++++++++++++ 10 files changed, 426 insertions(+), 25 deletions(-) create mode 100644 backend/apps/nest/migrations/0004_membergooglecredentials.py create mode 100644 backend/apps/nest/migrations/0005_alter_membergooglecredentials_options_and_more.py rename backend/apps/{slack/models/google_auth.py => nest/models/member_google_credentials.py} (84%) create mode 100644 backend/apps/slack/migrations/0023_delete_googleauth.py create mode 100644 backend/tests/apps/nest/models/member_google_credentials_test.py diff --git a/backend/.env.example b/backend/.env.example index 53d6c576f9..3da6b66940 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -13,8 +13,10 @@ DJANGO_DB_USER=None DJANGO_GOOGLE_AUTH_AUTH_URI=https://accounts.google.com/o/oauth2/auth DJANGO_GOOGLE_AUTH_CLIENT_ID=None DJANGO_GOOGLE_AUTH_CLIENT_SECRET=None -DJANGO_GOOGLE_AUTH_REDIRECT_URI=http://localhost:8000/integrations/slack/oauth2/callback/ +DJANGO_GOOGLE_AUTH_REDIRECT_URI=http://localhost:8000/auth/google/callback/ +DJANGO_GOOGLE_AUTH_SCOPES=https://www.googleapis.com/auth/calendar.readonly DJANGO_GOOGLE_AUTH_TOKEN_URI=https://oauth2.googleapis.com/token +DJANGO_IS_GOOGLE_AUTH_ENABLED=False DJANGO_OPEN_AI_SECRET_KEY=None DJANGO_PUBLIC_IP_ADDRESS="127.0.0.1" DJANGO_REDIS_HOST=None diff --git a/backend/apps/common/clients.py b/backend/apps/common/clients.py index f9f1e63396..68f1e1fd07 100644 --- a/backend/apps/common/clients.py +++ b/backend/apps/common/clients.py @@ -16,5 +16,5 @@ def get_google_auth_client(): "token_uri": settings.GOOGLE_AUTH_TOKEN_URI, } }, - scopes=["https://www.googleapis.com/auth/calendar.readonly"], + scopes=settings.GOOGLE_AUTH_SCOPES, ) diff --git a/backend/apps/nest/migrations/0004_membergooglecredentials.py b/backend/apps/nest/migrations/0004_membergooglecredentials.py new file mode 100644 index 0000000000..000d7826cc --- /dev/null +++ b/backend/apps/nest/migrations/0004_membergooglecredentials.py @@ -0,0 +1,41 @@ +# Generated by Django 5.2.4 on 2025-08-20 18:53 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("nest", "0003_badge"), + ("slack", "0023_delete_googleauth"), + ] + + operations = [ + migrations.CreateModel( + name="MemberGoogleCredentials", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("access_token", models.BinaryField(null=True, verbose_name="Access Token")), + ("refresh_token", models.BinaryField(null=True, verbose_name="Refresh Token")), + ("expires_at", models.DateTimeField(null=True, verbose_name="Token Expiry")), + ( + "member", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="google_auth", + to="slack.member", + verbose_name="Slack Member", + ), + ), + ], + options={ + "verbose_name_plural": "Google Auths", + "db_table": "slack_google_auths", + }, + ), + ] diff --git a/backend/apps/nest/migrations/0005_alter_membergooglecredentials_options_and_more.py b/backend/apps/nest/migrations/0005_alter_membergooglecredentials_options_and_more.py new file mode 100644 index 0000000000..95c1622904 --- /dev/null +++ b/backend/apps/nest/migrations/0005_alter_membergooglecredentials_options_and_more.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.4 on 2025-08-20 18:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("nest", "0004_membergooglecredentials"), + ] + + operations = [ + migrations.AlterModelOptions( + name="membergooglecredentials", + options={"verbose_name_plural": "Member's Google Credentials"}, + ), + migrations.AlterModelTable( + name="membergooglecredentials", + table="nest_member_google_credentials", + ), + ] diff --git a/backend/apps/nest/models/__init__.py b/backend/apps/nest/models/__init__.py index 597b834442..5dd1ca2380 100644 --- a/backend/apps/nest/models/__init__.py +++ b/backend/apps/nest/models/__init__.py @@ -1,3 +1,4 @@ from .api_key import ApiKey from .badge import Badge +from .member_google_credentials import MemberGoogleCredentials from .user import User diff --git a/backend/apps/slack/models/google_auth.py b/backend/apps/nest/models/member_google_credentials.py similarity index 84% rename from backend/apps/slack/models/google_auth.py rename to backend/apps/nest/models/member_google_credentials.py index d94eb52176..6455e25ded 100644 --- a/backend/apps/slack/models/google_auth.py +++ b/backend/apps/nest/models/member_google_credentials.py @@ -15,12 +15,12 @@ ) -class GoogleAuth(models.Model): +class MemberGoogleCredentials(models.Model): """Model to store Google OAuth tokens for Slack integration.""" class Meta: - db_table = "slack_google_auths" - verbose_name_plural = "Google Auths" + db_table = "nest_member_google_credentials" + verbose_name_plural = "Member's Google Credentials" member = models.OneToOneField( "slack.Member", @@ -40,21 +40,21 @@ def authenticate(member): """Authenticate a member. Returns: - - GoogleAuth instance if a valid/refreshable token exists, or + - MemberGoogleCredentials instance if a valid/refreshable token exists, or - (authorization_url, state) tuple to complete the OAuth flow. """ if not settings.IS_GOOGLE_AUTH_ENABLED: raise ValueError(AUTH_ERROR_MESSAGE) - auth = GoogleAuth.objects.get_or_create(member=member)[0] + auth = MemberGoogleCredentials.objects.get_or_create(member=member)[0] if auth.access_token and not auth.is_token_expired: return auth if auth.access_token: # If the access token is present but expired, refresh it - GoogleAuth.refresh_access_token(auth) + MemberGoogleCredentials.refresh_access_token(auth) return auth # If no access token is present, redirect to Google OAuth - flow = GoogleAuth.get_flow() + flow = MemberGoogleCredentials.get_flow() flow.redirect_uri = settings.GOOGLE_AUTH_REDIRECT_URI state = member.slack_user_id return flow.authorization_url( @@ -65,7 +65,7 @@ def authenticate(member): @staticmethod def authenticate_callback(auth_response, member_id): - """Authenticate a member and return a GoogleAuth instance.""" + """Authenticate a member and return a MemberGoogleCredentials instance.""" if not settings.IS_GOOGLE_AUTH_ENABLED: raise ValueError(AUTH_ERROR_MESSAGE) @@ -76,9 +76,9 @@ def authenticate_callback(auth_response, member_id): error_message = f"Member with Slack ID {member_id} does not exist." raise ValidationError(error_message) from e - auth = GoogleAuth.objects.get_or_create(member=member)[0] + auth = MemberGoogleCredentials.objects.get_or_create(member=member)[0] # This is the first time authentication, so we need to fetch a new token - flow = GoogleAuth.get_flow() + flow = MemberGoogleCredentials.get_flow() flow.redirect_uri = settings.GOOGLE_AUTH_REDIRECT_URI flow.fetch_token(authorization_response=auth_response) auth.access_token = flow.credentials.token @@ -127,5 +127,5 @@ def refresh_access_token(auth): auth.save() def __str__(self): - """Return a string representation of the GoogleAuth instance.""" - return f"GoogleAuth(member={self.member})" + """Return a string representation of the MemberGoogleCredentials instance.""" + return f"MemberGoogleCredentials(member={self.member})" diff --git a/backend/apps/slack/migrations/0023_delete_googleauth.py b/backend/apps/slack/migrations/0023_delete_googleauth.py new file mode 100644 index 0000000000..a1639ffa0f --- /dev/null +++ b/backend/apps/slack/migrations/0023_delete_googleauth.py @@ -0,0 +1,15 @@ +# Generated by Django 5.2.4 on 2025-08-20 18:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("slack", "0022_alter_googleauth_options_alter_googleauth_table"), + ] + + operations = [ + migrations.DeleteModel( + name="GoogleAuth", + ), + ] diff --git a/backend/apps/slack/models/__init__.py b/backend/apps/slack/models/__init__.py index a51e1a3467..3bbe0878de 100644 --- a/backend/apps/slack/models/__init__.py +++ b/backend/apps/slack/models/__init__.py @@ -1,6 +1,5 @@ from .conversation import Conversation from .event import Event -from .google_auth import GoogleAuth from .member import Member from .message import Message from .workspace import Workspace diff --git a/backend/settings/base.py b/backend/settings/base.py index da4f7c7a5c..5dbc383264 100644 --- a/backend/settings/base.py +++ b/backend/settings/base.py @@ -19,16 +19,14 @@ class Base(Configuration): DEBUG = False GITHUB_APP_ID = None GITHUB_APP_INSTALLATION_ID = None - GOOGLE_AUTH_AUTH_URI = values.Value(environ_name="GOOGLE_AUTH_AUTH_URI") - GOOGLE_AUTH_CLIENT_ID = values.SecretValue(environ_name="GOOGLE_AUTH_CLIENT_ID") - GOOGLE_AUTH_CLIENT_SECRET = values.SecretValue(environ_name="GOOGLE_AUTH_CLIENT_SECRET") - GOOGLE_AUTH_REDIRECT_URI = values.Value(environ_name="GOOGLE_AUTH_REDIRECT_URI") - GOOGLE_AUTH_TOKEN_URI = values.Value(environ_name="GOOGLE_AUTH_TOKEN_URI") - - IS_GOOGLE_AUTH_ENABLED = all( - value not in (None, "None", "") - for value in (GOOGLE_AUTH_CLIENT_ID, GOOGLE_AUTH_CLIENT_SECRET, GOOGLE_AUTH_REDIRECT_URI) - ) + GOOGLE_AUTH_AUTH_URI = values.Value() + GOOGLE_AUTH_CLIENT_ID = values.SecretValue() + GOOGLE_AUTH_CLIENT_SECRET = values.SecretValue() + GOOGLE_AUTH_REDIRECT_URI = values.Value() + GOOGLE_AUTH_SCOPES = values.ListValue() + GOOGLE_AUTH_TOKEN_URI = values.Value() + + IS_GOOGLE_AUTH_ENABLED = values.BooleanValue(default=False) IS_LOCAL_ENVIRONMENT = False IS_PRODUCTION_ENVIRONMENT = False IS_STAGING_ENVIRONMENT = False diff --git a/backend/tests/apps/nest/models/member_google_credentials_test.py b/backend/tests/apps/nest/models/member_google_credentials_test.py new file mode 100644 index 0000000000..fc77fac651 --- /dev/null +++ b/backend/tests/apps/nest/models/member_google_credentials_test.py @@ -0,0 +1,325 @@ +"""Tests for MemberGoogleCredentials model.""" + +from datetime import timedelta +from unittest.mock import Mock, patch + +import pytest +from django.conf import settings +from django.core.exceptions import ValidationError +from django.test import override_settings +from django.utils import timezone +from google_auth_oauthlib.flow import Flow + +from apps.nest.models.member_google_credentials import MemberGoogleCredentials +from apps.slack.models.member import Member + + +class TestMemberGoogleCredentialsModel: + """Test cases for MemberGoogleCredentials model.""" + + @pytest.fixture(autouse=True) + def setUp(self): + """Set up test data.""" + self.member = Member(slack_user_id="U123456789", username="testuser") + self.valid_token = b"valid_token" + self.valid_refresh_token = b"valid_refresh_token" + self.expired_time = timezone.now() - timedelta(hours=1) + self.future_time = timezone.now() + timedelta(hours=1) + + def test_member_google_credentials_creation(self): + """Test MemberGoogleCredentials model creation.""" + auth = MemberGoogleCredentials( + member=self.member, + access_token=self.valid_token, + refresh_token=self.valid_refresh_token, + expires_at=self.future_time, + ) + + assert auth.member == self.member + assert auth.access_token == self.valid_token + assert auth.refresh_token == self.valid_refresh_token + assert auth.expires_at == self.future_time + + def test_string_representation(self): + """Test string representation of MemberGoogleCredentials.""" + auth = MemberGoogleCredentials(member=self.member, access_token=self.valid_token) + + expected = f"MemberGoogleCredentials(member={self.member})" + assert str(auth) == expected + + def test_one_to_one_relationship(self): + """Test one-to-one relationship with Member.""" + auth = MemberGoogleCredentials(member=self.member, access_token=self.valid_token) + + assert self.member.member_google_credentials == auth + assert auth.member == self.member + + def test_is_token_expired_with_future_expiry(self): + """Test is_token_expired property with future expiry.""" + auth = MemberGoogleCredentials( + member=self.member, access_token=self.valid_token, expires_at=self.future_time + ) + + assert not auth.is_token_expired + + def test_is_token_expired_with_past_expiry(self): + """Test is_token_expired property with past expiry.""" + auth = MemberGoogleCredentials( + member=self.member, access_token=self.valid_token, expires_at=self.expired_time + ) + + assert auth.is_token_expired + + def test_is_token_expired_with_none_expiry(self): + """Test is_token_expired property with None expiry.""" + auth = MemberGoogleCredentials( + member=self.member, access_token=self.valid_token, expires_at=None + ) + + assert auth.is_token_expired + + @override_settings(IS_GOOGLE_AUTH_ENABLED=False) + def test_get_flow_when_disabled(self): + """Test get_flow raises error when Google auth is disabled.""" + with pytest.raises(ValueError, match="Google OAuth client ID"): + MemberGoogleCredentials.get_flow() + + @override_settings(IS_GOOGLE_AUTH_ENABLED=False) + def test_authenticate_when_disabled(self): + """Test authenticate raises error when Google auth is disabled.""" + with pytest.raises(ValueError, match="Google OAuth client ID"): + MemberGoogleCredentials.authenticate(self.member) + + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", + ) + @patch("apps.slack.models.member_google_credentials.MemberGoogleCredentials.save") + @patch( + "apps.slack.models.member_google_credentials.MemberGoogleCredentials.objects.get_or_create" + ) + @patch("apps.slack.models.member_google_credentials.MemberGoogleCredentials.get_flow") + def test_authenticate_existing_valid_token(self, mock_get_flow, mock_get_or_create, mock_save): + """Test authenticate with existing valid token.""" + # Create existing auth with valid token + + mock_get_flow.return_value = Mock(spec=Flow) + mock_get_or_create.return_value = ( + MemberGoogleCredentials( + member=self.member, + access_token=self.valid_token, + refresh_token=self.valid_refresh_token, + expires_at=self.future_time, + ), + True, + ) + result = MemberGoogleCredentials.authenticate(self.member) + + assert result.access_token == self.valid_token + assert result.refresh_token == self.valid_refresh_token + assert result.expires_at == self.future_time + mock_get_or_create.assert_called_once_with(member=self.member) + mock_save.assert_not_called() + + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", + ) + @patch( + "apps.slack.models.member_google_credentials.MemberGoogleCredentials.refresh_access_token" + ) + @patch( + "apps.slack.models.member_google_credentials.MemberGoogleCredentials.objects.get_or_create" + ) + def test_authenticate_existing_expired_token(self, mock_get_or_create, mock_refresh): + """Test authenticate with existing expired token.""" + # Create existing auth with expired token + existing_auth = MemberGoogleCredentials( + member=self.member, + access_token=self.valid_token, + refresh_token=self.valid_refresh_token, + expires_at=self.expired_time, + ) + mock_get_or_create.return_value = (existing_auth, False) + + MemberGoogleCredentials.authenticate(self.member) + + mock_refresh.assert_called_once_with(existing_auth) + mock_get_or_create.assert_called_once_with(member=self.member) + + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", + ) + @patch("apps.slack.models.member_google_credentials.MemberGoogleCredentials.get_flow") + @patch( + "apps.slack.models.member_google_credentials.MemberGoogleCredentials.objects.get_or_create" + ) + def test_authenticate_first_time(self, mock_get_or_create, mock_get_flow): + """Test authenticate for first time (no existing token).""" + # Mock flow and credentials + mock_flow_instance = Mock() + mock_get_flow.return_value = mock_flow_instance + mock_get_or_create.return_value = ( + MemberGoogleCredentials( + member=self.member, + access_token=None, + refresh_token=None, + expires_at=None, + ), + True, + ) + MemberGoogleCredentials.authenticate(self.member) + + mock_get_or_create.assert_called_once_with(member=self.member) + + mock_flow_instance.authorization_url.assert_called_once_with( + access_type="offline", + prompt="consent", + state=self.member.slack_user_id, + ) + + @override_settings(IS_GOOGLE_AUTH_ENABLED=False) + def test_refresh_access_token_when_disabled(self): + """Test refresh_access_token raises error when Google auth is disabled.""" + auth = MemberGoogleCredentials( + member=self.member, + access_token=self.valid_token, + refresh_token=self.valid_refresh_token, + ) + + with pytest.raises(ValueError, match="Google OAuth client ID"): + MemberGoogleCredentials.refresh_access_token(auth) + + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", + ) + @patch("apps.slack.models.member_google_credentials.Credentials") + @patch("apps.slack.models.member_google_credentials.Request") + @patch("apps.slack.models.member_google_credentials.MemberGoogleCredentials.save") + def test_refresh_access_token_success(self, mock_save, mock_request, mock_credentials): + """Test successful refresh_access_token.""" + # Create auth with refresh token + auth = MemberGoogleCredentials( + member=self.member, + access_token=self.valid_token, + refresh_token=self.valid_refresh_token, + expires_at=self.expired_time, + ) + + # Mock flow and new credentials + mock_credentials_instance = Mock() + mock_credentials_instance.token = b"token" # NOSONAR + mock_credentials_instance.refresh_token = b"refresh_token" + mock_credentials_instance.expiry = self.future_time + + mock_credentials.return_value = mock_credentials_instance + + MemberGoogleCredentials.refresh_access_token(auth) + + assert auth.access_token == b"token" + assert auth.refresh_token == b"refresh_token" + assert auth.expires_at == self.future_time + + mock_credentials.assert_called_once_with( + token=self.valid_token, + refresh_token=self.valid_refresh_token, + token_uri=settings.GOOGLE_AUTH_TOKEN_URI, + client_id=settings.GOOGLE_AUTH_CLIENT_ID, + client_secret=settings.GOOGLE_AUTH_CLIENT_SECRET, + ) + mock_credentials_instance.refresh.assert_called_once_with(mock_request.return_value) + mock_save.assert_called_once() + + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", + ) + def test_refresh_token_not_found(self): + """Test refresh_access_token raises error when no refresh token is present.""" + auth = MemberGoogleCredentials( + member=self.member, + access_token=self.valid_token, + refresh_token=None, + ) + + with pytest.raises( + ValidationError, match="Google OAuth refresh token is not set or expired." + ): + MemberGoogleCredentials.refresh_access_token(auth) + + def test_verbose_names(self): + """Test model field verbose names.""" + auth = MemberGoogleCredentials(member=self.member, access_token=self.valid_token) + + assert auth._meta.get_field("member").verbose_name == "Slack Member" + assert auth._meta.get_field("access_token").verbose_name == "Access Token" + assert auth._meta.get_field("refresh_token").verbose_name == "Refresh Token" + assert auth._meta.get_field("expires_at").verbose_name == "Token Expiry" + + @override_settings(IS_GOOGLE_AUTH_ENABLED=False) + def test_authenticate_callback_member_google_credentials_disabled(self): + """Test authenticate_callback raises error when Google auth is disabled.""" + with pytest.raises(ValueError, match="Google OAuth client ID"): + MemberGoogleCredentials.authenticate_callback(auth_response={}, member_id=4) + + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", + ) + @patch("apps.slack.models.member_google_credentials.MemberGoogleCredentials.get_flow") + @patch( + "apps.slack.models.member_google_credentials.MemberGoogleCredentials.objects.get_or_create" + ) + @patch("apps.slack.models.member_google_credentials.MemberGoogleCredentials.save") + @patch("apps.slack.models.member_google_credentials.Member.objects.get") + def test_authenticate_callback_success( + self, mock_member_get, mock_save, mock_get_or_create, mock_get_flow + ): + """Test successful authenticate_callback.""" + mock_credentials = Mock() + mock_credentials.token = b"token" # NOSONAR + mock_credentials.refresh_token = b"refresh_token" + mock_credentials.expiry = self.future_time + + mock_flow_instance = Mock(spec=Flow) + mock_flow_instance.credentials = mock_credentials + mock_member_get.return_value = self.member + mock_get_flow.return_value = mock_flow_instance + mock_get_or_create.return_value = (MemberGoogleCredentials(member=self.member), False) + result = MemberGoogleCredentials.authenticate_callback( + {}, member_id=self.member.slack_user_id + ) + + assert result.access_token == b"token" + assert result.refresh_token == b"refresh_token" + assert result.expires_at == self.future_time + mock_get_or_create.assert_called_once_with(member=self.member) + mock_save.assert_called_once() + mock_flow_instance.fetch_token.assert_called_once_with(authorization_response={}) + + @override_settings( + IS_GOOGLE_AUTH_ENABLED=True, + GOOGLE_AUTH_CLIENT_ID="test_client_id", + GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 + GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", + ) + @patch("apps.slack.models.member.Member.objects.get") + def test_authenticate_callback_member_not_found(self, mock_member_get): + """Test authenticate_callback raises error when member is not found.""" + mock_member_get.side_effect = Member.DoesNotExist + with pytest.raises(ValidationError, match="Member with Slack ID 4 does not exist."): + MemberGoogleCredentials.authenticate_callback(auth_response={}, member_id=4) From 68df7de666ca33657ea927d5d851182a116484d2 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 20 Aug 2025 22:10:11 +0300 Subject: [PATCH 24/25] Update tests and member related name --- ...06_alter_membergooglecredentials_member.py | 24 ++ .../nest/models/member_google_credentials.py | 2 +- .../models/member_google_credentials_test.py | 28 +- .../apps/slack/models/google_auth_test.py | 311 ------------------ 4 files changed, 39 insertions(+), 326 deletions(-) create mode 100644 backend/apps/nest/migrations/0006_alter_membergooglecredentials_member.py delete mode 100644 backend/tests/apps/slack/models/google_auth_test.py diff --git a/backend/apps/nest/migrations/0006_alter_membergooglecredentials_member.py b/backend/apps/nest/migrations/0006_alter_membergooglecredentials_member.py new file mode 100644 index 0000000000..c513d30a09 --- /dev/null +++ b/backend/apps/nest/migrations/0006_alter_membergooglecredentials_member.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.4 on 2025-08-20 19:08 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("nest", "0005_alter_membergooglecredentials_options_and_more"), + ("slack", "0023_delete_googleauth"), + ] + + operations = [ + migrations.AlterField( + model_name="membergooglecredentials", + name="member", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="member_google_credentials", + to="slack.member", + verbose_name="Slack Member", + ), + ), + ] diff --git a/backend/apps/nest/models/member_google_credentials.py b/backend/apps/nest/models/member_google_credentials.py index 6455e25ded..35b5fc7622 100644 --- a/backend/apps/nest/models/member_google_credentials.py +++ b/backend/apps/nest/models/member_google_credentials.py @@ -25,7 +25,7 @@ class Meta: member = models.OneToOneField( "slack.Member", on_delete=models.CASCADE, - related_name="google_auth", + related_name="member_google_credentials", verbose_name="Slack Member", ) access_token = models.BinaryField(verbose_name="Access Token", null=True) diff --git a/backend/tests/apps/nest/models/member_google_credentials_test.py b/backend/tests/apps/nest/models/member_google_credentials_test.py index fc77fac651..54a77a929e 100644 --- a/backend/tests/apps/nest/models/member_google_credentials_test.py +++ b/backend/tests/apps/nest/models/member_google_credentials_test.py @@ -96,11 +96,11 @@ def test_authenticate_when_disabled(self): GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", ) - @patch("apps.slack.models.member_google_credentials.MemberGoogleCredentials.save") + @patch("apps.nest.models.member_google_credentials.MemberGoogleCredentials.save") @patch( - "apps.slack.models.member_google_credentials.MemberGoogleCredentials.objects.get_or_create" + "apps.nest.models.member_google_credentials.MemberGoogleCredentials.objects.get_or_create" ) - @patch("apps.slack.models.member_google_credentials.MemberGoogleCredentials.get_flow") + @patch("apps.nest.models.member_google_credentials.MemberGoogleCredentials.get_flow") def test_authenticate_existing_valid_token(self, mock_get_flow, mock_get_or_create, mock_save): """Test authenticate with existing valid token.""" # Create existing auth with valid token @@ -130,10 +130,10 @@ def test_authenticate_existing_valid_token(self, mock_get_flow, mock_get_or_crea GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", ) @patch( - "apps.slack.models.member_google_credentials.MemberGoogleCredentials.refresh_access_token" + "apps.nest.models.member_google_credentials.MemberGoogleCredentials.refresh_access_token" ) @patch( - "apps.slack.models.member_google_credentials.MemberGoogleCredentials.objects.get_or_create" + "apps.nest.models.member_google_credentials.MemberGoogleCredentials.objects.get_or_create" ) def test_authenticate_existing_expired_token(self, mock_get_or_create, mock_refresh): """Test authenticate with existing expired token.""" @@ -157,9 +157,9 @@ def test_authenticate_existing_expired_token(self, mock_get_or_create, mock_refr GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", ) - @patch("apps.slack.models.member_google_credentials.MemberGoogleCredentials.get_flow") + @patch("apps.nest.models.member_google_credentials.MemberGoogleCredentials.get_flow") @patch( - "apps.slack.models.member_google_credentials.MemberGoogleCredentials.objects.get_or_create" + "apps.nest.models.member_google_credentials.MemberGoogleCredentials.objects.get_or_create" ) def test_authenticate_first_time(self, mock_get_or_create, mock_get_flow): """Test authenticate for first time (no existing token).""" @@ -203,9 +203,9 @@ def test_refresh_access_token_when_disabled(self): GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", ) - @patch("apps.slack.models.member_google_credentials.Credentials") - @patch("apps.slack.models.member_google_credentials.Request") - @patch("apps.slack.models.member_google_credentials.MemberGoogleCredentials.save") + @patch("apps.nest.models.member_google_credentials.Credentials") + @patch("apps.nest.models.member_google_credentials.Request") + @patch("apps.nest.models.member_google_credentials.MemberGoogleCredentials.save") def test_refresh_access_token_success(self, mock_save, mock_request, mock_credentials): """Test successful refresh_access_token.""" # Create auth with refresh token @@ -280,12 +280,12 @@ def test_authenticate_callback_member_google_credentials_disabled(self): GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", ) - @patch("apps.slack.models.member_google_credentials.MemberGoogleCredentials.get_flow") + @patch("apps.nest.models.member_google_credentials.MemberGoogleCredentials.get_flow") @patch( - "apps.slack.models.member_google_credentials.MemberGoogleCredentials.objects.get_or_create" + "apps.nest.models.member_google_credentials.MemberGoogleCredentials.objects.get_or_create" ) - @patch("apps.slack.models.member_google_credentials.MemberGoogleCredentials.save") - @patch("apps.slack.models.member_google_credentials.Member.objects.get") + @patch("apps.nest.models.member_google_credentials.MemberGoogleCredentials.save") + @patch("apps.nest.models.member_google_credentials.Member.objects.get") def test_authenticate_callback_success( self, mock_member_get, mock_save, mock_get_or_create, mock_get_flow ): diff --git a/backend/tests/apps/slack/models/google_auth_test.py b/backend/tests/apps/slack/models/google_auth_test.py deleted file mode 100644 index e8d2a010ca..0000000000 --- a/backend/tests/apps/slack/models/google_auth_test.py +++ /dev/null @@ -1,311 +0,0 @@ -"""Tests for GoogleAuth model.""" - -from datetime import timedelta -from unittest.mock import Mock, patch - -import pytest -from django.conf import settings -from django.core.exceptions import ValidationError -from django.test import override_settings -from django.utils import timezone -from google_auth_oauthlib.flow import Flow - -from apps.slack.models.google_auth import GoogleAuth -from apps.slack.models.member import Member - - -class TestGoogleAuthModel: - """Test cases for GoogleAuth model.""" - - @pytest.fixture(autouse=True) - def setUp(self): - """Set up test data.""" - self.member = Member(slack_user_id="U123456789", username="testuser") - self.valid_token = b"valid_token" - self.valid_refresh_token = b"valid_refresh_token" - self.expired_time = timezone.now() - timedelta(hours=1) - self.future_time = timezone.now() + timedelta(hours=1) - - def test_google_auth_creation(self): - """Test GoogleAuth model creation.""" - auth = GoogleAuth( - member=self.member, - access_token=self.valid_token, - refresh_token=self.valid_refresh_token, - expires_at=self.future_time, - ) - - assert auth.member == self.member - assert auth.access_token == self.valid_token - assert auth.refresh_token == self.valid_refresh_token - assert auth.expires_at == self.future_time - - def test_string_representation(self): - """Test string representation of GoogleAuth.""" - auth = GoogleAuth(member=self.member, access_token=self.valid_token) - - expected = f"GoogleAuth(member={self.member})" - assert str(auth) == expected - - def test_one_to_one_relationship(self): - """Test one-to-one relationship with Member.""" - auth = GoogleAuth(member=self.member, access_token=self.valid_token) - - assert self.member.google_auth == auth - assert auth.member == self.member - - def test_is_token_expired_with_future_expiry(self): - """Test is_token_expired property with future expiry.""" - auth = GoogleAuth( - member=self.member, access_token=self.valid_token, expires_at=self.future_time - ) - - assert not auth.is_token_expired - - def test_is_token_expired_with_past_expiry(self): - """Test is_token_expired property with past expiry.""" - auth = GoogleAuth( - member=self.member, access_token=self.valid_token, expires_at=self.expired_time - ) - - assert auth.is_token_expired - - def test_is_token_expired_with_none_expiry(self): - """Test is_token_expired property with None expiry.""" - auth = GoogleAuth(member=self.member, access_token=self.valid_token, expires_at=None) - - assert auth.is_token_expired - - @override_settings(IS_GOOGLE_AUTH_ENABLED=False) - def test_get_flow_when_disabled(self): - """Test get_flow raises error when Google auth is disabled.""" - with pytest.raises(ValueError, match="Google OAuth client ID"): - GoogleAuth.get_flow() - - @override_settings(IS_GOOGLE_AUTH_ENABLED=False) - def test_authenticate_when_disabled(self): - """Test authenticate raises error when Google auth is disabled.""" - with pytest.raises(ValueError, match="Google OAuth client ID"): - GoogleAuth.authenticate(self.member) - - @override_settings( - IS_GOOGLE_AUTH_ENABLED=True, - GOOGLE_AUTH_CLIENT_ID="test_client_id", - GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 - GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", - ) - @patch("apps.slack.models.google_auth.GoogleAuth.save") - @patch("apps.slack.models.google_auth.GoogleAuth.objects.get_or_create") - @patch("apps.slack.models.google_auth.GoogleAuth.get_flow") - def test_authenticate_existing_valid_token(self, mock_get_flow, mock_get_or_create, mock_save): - """Test authenticate with existing valid token.""" - # Create existing auth with valid token - - mock_get_flow.return_value = Mock(spec=Flow) - mock_get_or_create.return_value = ( - GoogleAuth( - member=self.member, - access_token=self.valid_token, - refresh_token=self.valid_refresh_token, - expires_at=self.future_time, - ), - True, - ) - result = GoogleAuth.authenticate(self.member) - - assert result.access_token == self.valid_token - assert result.refresh_token == self.valid_refresh_token - assert result.expires_at == self.future_time - mock_get_or_create.assert_called_once_with(member=self.member) - mock_save.assert_not_called() - - @override_settings( - IS_GOOGLE_AUTH_ENABLED=True, - GOOGLE_AUTH_CLIENT_ID="test_client_id", - GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 - GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", - ) - @patch("apps.slack.models.google_auth.GoogleAuth.refresh_access_token") - @patch("apps.slack.models.google_auth.GoogleAuth.objects.get_or_create") - def test_authenticate_existing_expired_token(self, mock_get_or_create, mock_refresh): - """Test authenticate with existing expired token.""" - # Create existing auth with expired token - existing_auth = GoogleAuth( - member=self.member, - access_token=self.valid_token, - refresh_token=self.valid_refresh_token, - expires_at=self.expired_time, - ) - mock_get_or_create.return_value = (existing_auth, False) - - GoogleAuth.authenticate(self.member) - - mock_refresh.assert_called_once_with(existing_auth) - mock_get_or_create.assert_called_once_with(member=self.member) - - @override_settings( - IS_GOOGLE_AUTH_ENABLED=True, - GOOGLE_AUTH_CLIENT_ID="test_client_id", - GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 - GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", - ) - @patch("apps.slack.models.google_auth.GoogleAuth.get_flow") - @patch("apps.slack.models.google_auth.GoogleAuth.objects.get_or_create") - def test_authenticate_first_time(self, mock_get_or_create, mock_get_flow): - """Test authenticate for first time (no existing token).""" - # Mock flow and credentials - mock_flow_instance = Mock() - mock_get_flow.return_value = mock_flow_instance - mock_get_or_create.return_value = ( - GoogleAuth( - member=self.member, - access_token=None, - refresh_token=None, - expires_at=None, - ), - True, - ) - GoogleAuth.authenticate(self.member) - - mock_get_or_create.assert_called_once_with(member=self.member) - - mock_flow_instance.authorization_url.assert_called_once_with( - access_type="offline", - prompt="consent", - state=self.member.slack_user_id, - ) - - @override_settings(IS_GOOGLE_AUTH_ENABLED=False) - def test_refresh_access_token_when_disabled(self): - """Test refresh_access_token raises error when Google auth is disabled.""" - auth = GoogleAuth( - member=self.member, - access_token=self.valid_token, - refresh_token=self.valid_refresh_token, - ) - - with pytest.raises(ValueError, match="Google OAuth client ID"): - GoogleAuth.refresh_access_token(auth) - - @override_settings( - IS_GOOGLE_AUTH_ENABLED=True, - GOOGLE_AUTH_CLIENT_ID="test_client_id", - GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 - GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", - ) - @patch("apps.slack.models.google_auth.Credentials") - @patch("apps.slack.models.google_auth.Request") - @patch("apps.slack.models.google_auth.GoogleAuth.save") - def test_refresh_access_token_success(self, mock_save, mock_request, mock_credentials): - """Test successful refresh_access_token.""" - # Create auth with refresh token - auth = GoogleAuth( - member=self.member, - access_token=self.valid_token, - refresh_token=self.valid_refresh_token, - expires_at=self.expired_time, - ) - - # Mock flow and new credentials - mock_credentials_instance = Mock() - mock_credentials_instance.token = b"token" # NOSONAR - mock_credentials_instance.refresh_token = b"refresh_token" - mock_credentials_instance.expiry = self.future_time - - mock_credentials.return_value = mock_credentials_instance - - GoogleAuth.refresh_access_token(auth) - - assert auth.access_token == b"token" - assert auth.refresh_token == b"refresh_token" - assert auth.expires_at == self.future_time - - mock_credentials.assert_called_once_with( - token=self.valid_token, - refresh_token=self.valid_refresh_token, - token_uri=settings.GOOGLE_AUTH_TOKEN_URI, - client_id=settings.GOOGLE_AUTH_CLIENT_ID, - client_secret=settings.GOOGLE_AUTH_CLIENT_SECRET, - ) - mock_credentials_instance.refresh.assert_called_once_with(mock_request.return_value) - mock_save.assert_called_once() - - @override_settings( - IS_GOOGLE_AUTH_ENABLED=True, - GOOGLE_AUTH_CLIENT_ID="test_client_id", - GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 - GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", - ) - def test_refresh_token_not_found(self): - """Test refresh_access_token raises error when no refresh token is present.""" - auth = GoogleAuth( - member=self.member, - access_token=self.valid_token, - refresh_token=None, - ) - - with pytest.raises( - ValidationError, match="Google OAuth refresh token is not set or expired." - ): - GoogleAuth.refresh_access_token(auth) - - def test_verbose_names(self): - """Test model field verbose names.""" - auth = GoogleAuth(member=self.member, access_token=self.valid_token) - - assert auth._meta.get_field("member").verbose_name == "Slack Member" - assert auth._meta.get_field("access_token").verbose_name == "Access Token" - assert auth._meta.get_field("refresh_token").verbose_name == "Refresh Token" - assert auth._meta.get_field("expires_at").verbose_name == "Token Expiry" - - @override_settings(IS_GOOGLE_AUTH_ENABLED=False) - def test_authenticate_callback_google_auth_disabled(self): - """Test authenticate_callback raises error when Google auth is disabled.""" - with pytest.raises(ValueError, match="Google OAuth client ID"): - GoogleAuth.authenticate_callback(auth_response={}, member_id=4) - - @override_settings( - IS_GOOGLE_AUTH_ENABLED=True, - GOOGLE_AUTH_CLIENT_ID="test_client_id", - GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 - GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", - ) - @patch("apps.slack.models.google_auth.GoogleAuth.get_flow") - @patch("apps.slack.models.google_auth.GoogleAuth.objects.get_or_create") - @patch("apps.slack.models.google_auth.GoogleAuth.save") - @patch("apps.slack.models.google_auth.Member.objects.get") - def test_authenticate_callback_success( - self, mock_member_get, mock_save, mock_get_or_create, mock_get_flow - ): - """Test successful authenticate_callback.""" - mock_credentials = Mock() - mock_credentials.token = b"token" # NOSONAR - mock_credentials.refresh_token = b"refresh_token" - mock_credentials.expiry = self.future_time - - mock_flow_instance = Mock(spec=Flow) - mock_flow_instance.credentials = mock_credentials - mock_member_get.return_value = self.member - mock_get_flow.return_value = mock_flow_instance - mock_get_or_create.return_value = (GoogleAuth(member=self.member), False) - result = GoogleAuth.authenticate_callback({}, member_id=self.member.slack_user_id) - - assert result.access_token == b"token" - assert result.refresh_token == b"refresh_token" - assert result.expires_at == self.future_time - mock_get_or_create.assert_called_once_with(member=self.member) - mock_save.assert_called_once() - mock_flow_instance.fetch_token.assert_called_once_with(authorization_response={}) - - @override_settings( - IS_GOOGLE_AUTH_ENABLED=True, - GOOGLE_AUTH_CLIENT_ID="test_client_id", - GOOGLE_AUTH_CLIENT_SECRET="test_client_secret", # noqa: S106 - GOOGLE_AUTH_REDIRECT_URI="http://localhost:8000/callback", - ) - @patch("apps.slack.models.member.Member.objects.get") - def test_authenticate_callback_member_not_found(self, mock_member_get): - """Test authenticate_callback raises error when member is not found.""" - mock_member_get.side_effect = Member.DoesNotExist - with pytest.raises(ValidationError, match="Member with Slack ID 4 does not exist."): - GoogleAuth.authenticate_callback(auth_response={}, member_id=4) From cf136a4fcfff1b3fd4dfe082cd8118bb15319130 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 21 Aug 2025 19:35:51 +0300 Subject: [PATCH 25/25] Clean up migrations --- .../0004_membergooglecredentials.py | 41 ------------------- ...embergooglecredentials_options_and_more.py | 20 --------- ...06_alter_membergooglecredentials_member.py | 24 ----------- .../apps/slack/migrations/0019_googleauth.py | 39 ------------------ ..._alter_googleauth_access_token_and_more.py | 22 ---------- ..._alter_googleauth_access_token_and_more.py | 27 ------------ ...ogleauth_options_alter_googleauth_table.py | 20 --------- .../migrations/0023_delete_googleauth.py | 15 ------- 8 files changed, 208 deletions(-) delete mode 100644 backend/apps/nest/migrations/0004_membergooglecredentials.py delete mode 100644 backend/apps/nest/migrations/0005_alter_membergooglecredentials_options_and_more.py delete mode 100644 backend/apps/nest/migrations/0006_alter_membergooglecredentials_member.py delete mode 100644 backend/apps/slack/migrations/0019_googleauth.py delete mode 100644 backend/apps/slack/migrations/0020_alter_googleauth_access_token_and_more.py delete mode 100644 backend/apps/slack/migrations/0021_alter_googleauth_access_token_and_more.py delete mode 100644 backend/apps/slack/migrations/0022_alter_googleauth_options_alter_googleauth_table.py delete mode 100644 backend/apps/slack/migrations/0023_delete_googleauth.py diff --git a/backend/apps/nest/migrations/0004_membergooglecredentials.py b/backend/apps/nest/migrations/0004_membergooglecredentials.py deleted file mode 100644 index 000d7826cc..0000000000 --- a/backend/apps/nest/migrations/0004_membergooglecredentials.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-20 18:53 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("nest", "0003_badge"), - ("slack", "0023_delete_googleauth"), - ] - - operations = [ - migrations.CreateModel( - name="MemberGoogleCredentials", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ("access_token", models.BinaryField(null=True, verbose_name="Access Token")), - ("refresh_token", models.BinaryField(null=True, verbose_name="Refresh Token")), - ("expires_at", models.DateTimeField(null=True, verbose_name="Token Expiry")), - ( - "member", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="google_auth", - to="slack.member", - verbose_name="Slack Member", - ), - ), - ], - options={ - "verbose_name_plural": "Google Auths", - "db_table": "slack_google_auths", - }, - ), - ] diff --git a/backend/apps/nest/migrations/0005_alter_membergooglecredentials_options_and_more.py b/backend/apps/nest/migrations/0005_alter_membergooglecredentials_options_and_more.py deleted file mode 100644 index 95c1622904..0000000000 --- a/backend/apps/nest/migrations/0005_alter_membergooglecredentials_options_and_more.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-20 18:55 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("nest", "0004_membergooglecredentials"), - ] - - operations = [ - migrations.AlterModelOptions( - name="membergooglecredentials", - options={"verbose_name_plural": "Member's Google Credentials"}, - ), - migrations.AlterModelTable( - name="membergooglecredentials", - table="nest_member_google_credentials", - ), - ] diff --git a/backend/apps/nest/migrations/0006_alter_membergooglecredentials_member.py b/backend/apps/nest/migrations/0006_alter_membergooglecredentials_member.py deleted file mode 100644 index c513d30a09..0000000000 --- a/backend/apps/nest/migrations/0006_alter_membergooglecredentials_member.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-20 19:08 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("nest", "0005_alter_membergooglecredentials_options_and_more"), - ("slack", "0023_delete_googleauth"), - ] - - operations = [ - migrations.AlterField( - model_name="membergooglecredentials", - name="member", - field=models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="member_google_credentials", - to="slack.member", - verbose_name="Slack Member", - ), - ), - ] diff --git a/backend/apps/slack/migrations/0019_googleauth.py b/backend/apps/slack/migrations/0019_googleauth.py deleted file mode 100644 index 65c0a0c9fe..0000000000 --- a/backend/apps/slack/migrations/0019_googleauth.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-11 04:45 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("slack", "0018_conversation_sync_messages"), - ] - - operations = [ - migrations.CreateModel( - name="GoogleAuth", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ("access_token", models.TextField(blank=True, verbose_name="Access Token")), - ("refresh_token", models.TextField(blank=True, verbose_name="Refresh Token")), - ( - "expires_at", - models.DateTimeField(blank=True, null=True, verbose_name="Token Expiry"), - ), - ( - "member", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="google_auth", - to="slack.member", - verbose_name="Slack Member", - ), - ), - ], - ), - ] diff --git a/backend/apps/slack/migrations/0020_alter_googleauth_access_token_and_more.py b/backend/apps/slack/migrations/0020_alter_googleauth_access_token_and_more.py deleted file mode 100644 index dfa1cee5a9..0000000000 --- a/backend/apps/slack/migrations/0020_alter_googleauth_access_token_and_more.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-12 17:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("slack", "0019_googleauth"), - ] - - operations = [ - migrations.AlterField( - model_name="googleauth", - name="access_token", - field=models.BinaryField(blank=True, verbose_name="Access Token"), - ), - migrations.AlterField( - model_name="googleauth", - name="refresh_token", - field=models.BinaryField(blank=True, verbose_name="Refresh Token"), - ), - ] diff --git a/backend/apps/slack/migrations/0021_alter_googleauth_access_token_and_more.py b/backend/apps/slack/migrations/0021_alter_googleauth_access_token_and_more.py deleted file mode 100644 index 9880b4fe49..0000000000 --- a/backend/apps/slack/migrations/0021_alter_googleauth_access_token_and_more.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-13 16:01 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("slack", "0020_alter_googleauth_access_token_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="googleauth", - name="access_token", - field=models.BinaryField(null=True, verbose_name="Access Token"), - ), - migrations.AlterField( - model_name="googleauth", - name="expires_at", - field=models.DateTimeField(null=True, verbose_name="Token Expiry"), - ), - migrations.AlterField( - model_name="googleauth", - name="refresh_token", - field=models.BinaryField(null=True, verbose_name="Refresh Token"), - ), - ] diff --git a/backend/apps/slack/migrations/0022_alter_googleauth_options_alter_googleauth_table.py b/backend/apps/slack/migrations/0022_alter_googleauth_options_alter_googleauth_table.py deleted file mode 100644 index 02d75225c8..0000000000 --- a/backend/apps/slack/migrations/0022_alter_googleauth_options_alter_googleauth_table.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-16 12:19 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("slack", "0021_alter_googleauth_access_token_and_more"), - ] - - operations = [ - migrations.AlterModelOptions( - name="googleauth", - options={"verbose_name_plural": "Google Auths"}, - ), - migrations.AlterModelTable( - name="googleauth", - table="slack_google_auths", - ), - ] diff --git a/backend/apps/slack/migrations/0023_delete_googleauth.py b/backend/apps/slack/migrations/0023_delete_googleauth.py deleted file mode 100644 index a1639ffa0f..0000000000 --- a/backend/apps/slack/migrations/0023_delete_googleauth.py +++ /dev/null @@ -1,15 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-20 18:53 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("slack", "0022_alter_googleauth_options_alter_googleauth_table"), - ] - - operations = [ - migrations.DeleteModel( - name="GoogleAuth", - ), - ]