From 3e2570ae0b2d03cea2d75672c68aad4fff6843d7 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev Date: Wed, 7 Jan 2026 09:17:34 +0700 Subject: [PATCH] Initial commit from monorepo --- Dockerfile | 24 + README.md | 47 + db.sqlite3 | 0 manage.py | 17 + nixpacks.toml | 18 + poetry.lock | 1005 +++++++++++++++++ pyproject.toml | 28 + teams/__init__.py | 1 + teams/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 167 bytes teams/__pycache__/settings.cpython-313.pyc | Bin 0 -> 5291 bytes .../settings_local.cpython-313.pyc | Bin 0 -> 2005 bytes teams/__pycache__/urls.cpython-313.pyc | Bin 0 -> 1350 bytes teams/settings.py | 161 +++ teams/settings_local.py | 71 ++ teams/urls.py | 17 + teams/wsgi.py | 6 + teams_app/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 171 bytes teams_app/__pycache__/admin.cpython-313.pyc | Bin 0 -> 2989 bytes teams_app/__pycache__/apps.cpython-313.pyc | Bin 0 -> 540 bytes teams_app/__pycache__/auth.cpython-313.pyc | Bin 0 -> 3467 bytes .../graphql_middleware.cpython-313.pyc | Bin 0 -> 4408 bytes teams_app/__pycache__/models.cpython-313.pyc | Bin 0 -> 9636 bytes .../__pycache__/permissions.cpython-313.pyc | Bin 0 -> 3531 bytes teams_app/__pycache__/views.cpython-313.pyc | Bin 0 -> 4957 bytes teams_app/admin.py | 45 + teams_app/apps.py | 5 + teams_app/auth.py | 70 ++ teams_app/graphql_middleware.py | 81 ++ teams_app/middleware.py | 56 + teams_app/migrations/0001_initial.py | 68 ++ teams_app/migrations/0002_add_user_profile.py | 30 + teams_app/migrations/0003_add_team_type.py | 18 + teams_app/migrations/0004_teamaddress.py | 33 + .../0005_remove_team_prefect_flow_run_id.py | 17 + ...dd_address_status_and_selected_location.py | 59 + .../0007_teamaddress_country_code.py | 18 + ...graph_node_id_and_change_default_status.py | 31 + .../0009_alter_teamaddress_status.py | 18 + .../migrations/0010_remove_team_status.py | 17 + .../migrations/0011_teaminvitationtoken.py | 30 + .../0012_add_selected_location_details.py | 28 + teams_app/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-313.pyc | Bin 0 -> 4448 bytes .../0002_add_user_profile.cpython-313.pyc | Bin 0 -> 2148 bytes .../0003_add_team_type.cpython-313.pyc | Bin 0 -> 916 bytes .../0004_teamaddress.cpython-313.pyc | Bin 0 -> 1964 bytes ...e_team_prefect_flow_run_id.cpython-313.pyc | Bin 0 -> 723 bytes ...atus_and_selected_location.cpython-313.pyc | Bin 0 -> 1809 bytes ...7_teamaddress_country_code.cpython-313.pyc | Bin 0 -> 868 bytes ..._and_change_default_status.cpython-313.pyc | Bin 0 -> 1133 bytes ...9_alter_teamaddress_status.cpython-313.pyc | Bin 0 -> 1006 bytes .../0010_remove_team_status.cpython-313.pyc | Bin 0 -> 710 bytes .../0011_teaminvitationtoken.cpython-313.pyc | Bin 0 -> 1930 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 182 bytes teams_app/models.py | 165 +++ teams_app/permissions.py | 74 ++ .../__pycache__/m2m_schema.cpython-313.pyc | Bin 0 -> 18693 bytes .../__pycache__/public_schema.cpython-313.pyc | Bin 0 -> 883 bytes .../__pycache__/team_schema.cpython-313.pyc | Bin 0 -> 20307 bytes .../__pycache__/user_schema.cpython-313.pyc | Bin 0 -> 14149 bytes teams_app/schemas/m2m_schema.py | 329 ++++++ teams_app/schemas/public_schema.py | 12 + teams_app/schemas/team_schema.py | 367 ++++++ teams_app/schemas/user_schema.py | 287 +++++ teams_app/services.py | 160 +++ teams_app/temporal_client.py | 168 +++ teams_app/tests.py | 98 ++ teams_app/views.py | 97 ++ 69 files changed, 3777 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 db.sqlite3 create mode 100644 manage.py create mode 100644 nixpacks.toml create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 teams/__init__.py create mode 100644 teams/__pycache__/__init__.cpython-313.pyc create mode 100644 teams/__pycache__/settings.cpython-313.pyc create mode 100644 teams/__pycache__/settings_local.cpython-313.pyc create mode 100644 teams/__pycache__/urls.cpython-313.pyc create mode 100644 teams/settings.py create mode 100644 teams/settings_local.py create mode 100644 teams/urls.py create mode 100644 teams/wsgi.py create mode 100644 teams_app/__init__.py create mode 100644 teams_app/__pycache__/__init__.cpython-313.pyc create mode 100644 teams_app/__pycache__/admin.cpython-313.pyc create mode 100644 teams_app/__pycache__/apps.cpython-313.pyc create mode 100644 teams_app/__pycache__/auth.cpython-313.pyc create mode 100644 teams_app/__pycache__/graphql_middleware.cpython-313.pyc create mode 100644 teams_app/__pycache__/models.cpython-313.pyc create mode 100644 teams_app/__pycache__/permissions.cpython-313.pyc create mode 100644 teams_app/__pycache__/views.cpython-313.pyc create mode 100644 teams_app/admin.py create mode 100644 teams_app/apps.py create mode 100644 teams_app/auth.py create mode 100644 teams_app/graphql_middleware.py create mode 100644 teams_app/middleware.py create mode 100644 teams_app/migrations/0001_initial.py create mode 100644 teams_app/migrations/0002_add_user_profile.py create mode 100644 teams_app/migrations/0003_add_team_type.py create mode 100644 teams_app/migrations/0004_teamaddress.py create mode 100644 teams_app/migrations/0005_remove_team_prefect_flow_run_id.py create mode 100644 teams_app/migrations/0006_add_address_status_and_selected_location.py create mode 100644 teams_app/migrations/0007_teamaddress_country_code.py create mode 100644 teams_app/migrations/0008_remove_graph_node_id_and_change_default_status.py create mode 100644 teams_app/migrations/0009_alter_teamaddress_status.py create mode 100644 teams_app/migrations/0010_remove_team_status.py create mode 100644 teams_app/migrations/0011_teaminvitationtoken.py create mode 100644 teams_app/migrations/0012_add_selected_location_details.py create mode 100644 teams_app/migrations/__init__.py create mode 100644 teams_app/migrations/__pycache__/0001_initial.cpython-313.pyc create mode 100644 teams_app/migrations/__pycache__/0002_add_user_profile.cpython-313.pyc create mode 100644 teams_app/migrations/__pycache__/0003_add_team_type.cpython-313.pyc create mode 100644 teams_app/migrations/__pycache__/0004_teamaddress.cpython-313.pyc create mode 100644 teams_app/migrations/__pycache__/0005_remove_team_prefect_flow_run_id.cpython-313.pyc create mode 100644 teams_app/migrations/__pycache__/0006_add_address_status_and_selected_location.cpython-313.pyc create mode 100644 teams_app/migrations/__pycache__/0007_teamaddress_country_code.cpython-313.pyc create mode 100644 teams_app/migrations/__pycache__/0008_remove_graph_node_id_and_change_default_status.cpython-313.pyc create mode 100644 teams_app/migrations/__pycache__/0009_alter_teamaddress_status.cpython-313.pyc create mode 100644 teams_app/migrations/__pycache__/0010_remove_team_status.cpython-313.pyc create mode 100644 teams_app/migrations/__pycache__/0011_teaminvitationtoken.cpython-313.pyc create mode 100644 teams_app/migrations/__pycache__/__init__.cpython-313.pyc create mode 100644 teams_app/models.py create mode 100644 teams_app/permissions.py create mode 100644 teams_app/schemas/__pycache__/m2m_schema.cpython-313.pyc create mode 100644 teams_app/schemas/__pycache__/public_schema.cpython-313.pyc create mode 100644 teams_app/schemas/__pycache__/team_schema.cpython-313.pyc create mode 100644 teams_app/schemas/__pycache__/user_schema.cpython-313.pyc create mode 100644 teams_app/schemas/m2m_schema.py create mode 100644 teams_app/schemas/public_schema.py create mode 100644 teams_app/schemas/team_schema.py create mode 100644 teams_app/schemas/user_schema.py create mode 100644 teams_app/services.py create mode 100644 teams_app/temporal_client.py create mode 100644 teams_app/tests.py create mode 100644 teams_app/views.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..09bc067 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + NIXPACKS_POETRY_VERSION=2.2.1 + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential curl \ + && rm -rf /var/lib/apt/lists/* + +RUN python -m venv --copies /opt/venv +ENV VIRTUAL_ENV=/opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +COPY . . + +RUN pip install --no-cache-dir poetry==$NIXPACKS_POETRY_VERSION \ + && poetry install --no-interaction --no-ansi + +ENV PORT=8000 + +CMD ["sh", "-c", "poetry run python manage.py migrate && poetry run python manage.py collectstatic --noinput && poetry run python -m gunicorn teams.wsgi:application --bind 0.0.0.0:${PORT:-8000}"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..039d672 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Teams Service + +Backend сервис для управления командами и участниками в системе Optovia. + +## Описание + +Сервис для управления командами с интеграцией Logto для аутентификации. Включает управление участниками, приглашениями и KYC статусами команд. + +## Основные функции + +- Создание и управление командами +- Управление участниками команд (OWNER, ADMIN, MANAGER, MEMBER) +- Система приглашений в команды +- Интеграция с Logto для аутентификации +- KYC статусы команд +- Управление активной командой пользователя + +## Модели данных + +- **Team** - модель команды с KYC статусами +- **TeamMember** - участники команды с ролями +- **TeamInvitation** - приглашения в команды +- **User** - пользователи с привязкой к Logto + +## KYC статусы команд + +- `PENDING_KYC` - Требуется KYC +- `KYC_IN_REVIEW` - KYC на рассмотрении +- `KYC_APPROVED` - KYC одобрен +- `KYC_REJECTED` - KYC отклонен +- `SUSPENDED` - Заблокировано + +## Технологии + +- Django 5.2.8 +- GraphQL (Graphene-Django) +- PostgreSQL +- Logto Integration +- Gunicorn + +## Развертывание + +Проект развертывается через Nixpacks на Dokploy с автоматическими миграциями. + +## Автор + +Ruslan Bakiev diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000..e69de29 diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..df2eb1b --- /dev/null +++ b/manage.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + +if __name__ == '__main__': + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'teams.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) \ No newline at end of file diff --git a/nixpacks.toml b/nixpacks.toml new file mode 100644 index 0000000..b3161ac --- /dev/null +++ b/nixpacks.toml @@ -0,0 +1,18 @@ +providers = ["python"] + +[build] + +[phases.install] +cmds = [ + "python -m venv --copies /opt/venv", + ". /opt/venv/bin/activate", + "pip install poetry==$NIXPACKS_POETRY_VERSION", + "poetry install --no-interaction --no-ansi" +] + +[start] +cmd = "poetry run python manage.py migrate && poetry run python manage.py collectstatic --noinput && poetry run python -m gunicorn teams.wsgi:application --bind 0.0.0.0:${PORT:-8000}" + +[variables] +# Set Poetry version to match local environment +NIXPACKS_POETRY_VERSION = "2.2.1" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..edff392 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1005 @@ +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. + +[[package]] +name = "aenum" +version = "3.1.16" +description = "Advanced Enumerations (compatible with Python's stdlib Enum), NamedTuples, and NamedConstants" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "aenum-3.1.16-py2-none-any.whl", hash = "sha256:7810cbb6b4054b7654e5a7bafbe16e9ee1d25ef8e397be699f63f2f3a5800433"}, + {file = "aenum-3.1.16-py3-none-any.whl", hash = "sha256:9035092855a98e41b66e3d0998bd7b96280e85ceb3a04cc035636138a1943eaf"}, +] + +[[package]] +name = "asgiref" +version = "3.11.0" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d"}, + {file = "asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4"}, +] + +[package.extras] +tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] + +[[package]] +name = "boto3" +version = "1.41.4" +description = "The AWS SDK for Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "boto3-1.41.4-py3-none-any.whl", hash = "sha256:77d84b7ce890a9b0c6a8993f8de106d8cf8138f332a4685e6de453965e60cb24"}, + {file = "boto3-1.41.4.tar.gz", hash = "sha256:2c6b8d13df6beb9255d0c8cb60a7a2164f5270580ea5b921a65658a0c28e3223"}, +] + +[package.dependencies] +botocore = ">=1.41.4,<1.42.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.15.0,<0.16.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.41.4" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "botocore-1.41.4-py3-none-any.whl", hash = "sha256:7143ef845f1d1400dbbf05d999f8c5e8cfaecd6bd84cbfbe5fa0a40e3a9f6353"}, + {file = "botocore-1.41.4.tar.gz", hash = "sha256:45c78f07b53a64cbe55e5d60297958f151bd4a2c6acb944a8bb65874bc2fd953"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} + +[package.extras] +crt = ["awscrt (==0.29.0)"] + +[[package]] +name = "certifi" +version = "2025.11.12" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, + {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +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.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +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.8" +groups = ["main"] +files = [ + {file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"}, + {file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"}, + {file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"}, + {file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"}, + {file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"}, + {file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"}, + {file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"}, + {file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"}, + {file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"}, + {file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"}, + {file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} + +[package.extras] +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[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "django" +version = "5.2.8" +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.8-py3-none-any.whl", hash = "sha256:37e687f7bd73ddf043e2b6b97cfe02fcbb11f2dbb3adccc6a2b18c6daa054d7f"}, + {file = "django-5.2.8.tar.gz", hash = "sha256:23254866a5bb9a2cfa6004e8b809ec6246eba4b58a7589bc2772f1bcc8456c7f"}, +] + +[package.dependencies] +asgiref = ">=3.8.1" +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-cors-headers" +version = "4.9.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.9.0-py3-none-any.whl", hash = "sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449"}, + {file = "django_cors_headers-4.9.0.tar.gz", hash = "sha256:fe5d7cb59fdc2c8c646ce84b727ac2bca8912a247e6e68e1fb507372178e59e8"}, +] + +[package.dependencies] +asgiref = ">=3.6" +django = ">=4.2" + +[[package]] +name = "graphene" +version = "3.4.3" +description = "GraphQL Framework for Python" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "graphene-3.4.3-py2.py3-none-any.whl", hash = "sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71"}, + {file = "graphene-3.4.3.tar.gz", hash = "sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa"}, +] + +[package.dependencies] +graphql-core = ">=3.1,<3.3" +graphql-relay = ">=3.1,<3.3" +python-dateutil = ">=2.7.0,<3" +typing-extensions = ">=4.7.1,<5" + +[package.extras] +dev = ["coveralls (>=3.3,<5)", "mypy (>=1.10,<2)", "pytest (>=8,<9)", "pytest-asyncio (>=0.16,<2)", "pytest-benchmark (>=4,<5)", "pytest-cov (>=5,<6)", "pytest-mock (>=3,<4)", "ruff (==0.5.0)", "types-python-dateutil (>=2.8.1,<3)"] +test = ["coveralls (>=3.3,<5)", "pytest (>=8,<9)", "pytest-asyncio (>=0.16,<2)", "pytest-benchmark (>=4,<5)", "pytest-cov (>=5,<6)", "pytest-mock (>=3,<4)"] + +[[package]] +name = "graphene-django" +version = "3.2.3" +description = "Graphene Django integration" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "graphene-django-3.2.3.tar.gz", hash = "sha256:d831bfe8e9a6e77e477b7854faef4addb318f386119a69ee4c57b74560f3e07d"}, + {file = "graphene_django-3.2.3-py2.py3-none-any.whl", hash = "sha256:0c673a4dad315b26b4d18eb379ad0c7027fd6a36d23a1848b7c7c09a14a9271e"}, +] + +[package.dependencies] +Django = ">=3.2" +graphene = ">=3.0,<4" +graphql-core = ">=3.1.0,<4" +graphql-relay = ">=3.1.1,<4" +promise = ">=2.1" +text-unidecode = "*" + +[package.extras] +dev = ["coveralls", "django-filter (>=22.1)", "djangorestframework (>=3.6.3)", "mock", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-django (>=4.5.2)", "pytest-random-order", "pytz", "ruff (==0.1.2)"] +rest-framework = ["djangorestframework (>=3.6.3)"] +test = ["coveralls", "django-filter (>=22.1)", "djangorestframework (>=3.6.3)", "mock", "pytest (>=7.3.1)", "pytest-cov", "pytest-django (>=4.5.2)", "pytest-random-order", "pytz"] + +[[package]] +name = "graphql-core" +version = "3.2.7" +description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." +optional = false +python-versions = "<4,>=3.7" +groups = ["main"] +files = [ + {file = "graphql_core-3.2.7-py3-none-any.whl", hash = "sha256:17fc8f3ca4a42913d8e24d9ac9f08deddf0a0b2483076575757f6c412ead2ec0"}, + {file = "graphql_core-3.2.7.tar.gz", hash = "sha256:27b6904bdd3b43f2a0556dad5d579bdfdeab1f38e8e8788e555bdcb586a6f62c"}, +] + +[[package]] +name = "graphql-relay" +version = "3.2.0" +description = "Relay library for graphql-core" +optional = false +python-versions = ">=3.6,<4" +groups = ["main"] +files = [ + {file = "graphql-relay-3.2.0.tar.gz", hash = "sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c"}, + {file = "graphql_relay-3.2.0-py3-none-any.whl", hash = "sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5"}, +] + +[package.dependencies] +graphql-core = ">=3.2,<3.3" + +[[package]] +name = "gunicorn" +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"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "infisicalsdk" +version = "1.0.12" +description = "Official Infisical SDK for Python (Latest)" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "infisicalsdk-1.0.12-py3-none-any.whl", hash = "sha256:348af8f665fd81beac643db5a81f858eaf473e5809bdb7dfdcca923fe3ab49ea"}, + {file = "infisicalsdk-1.0.12.tar.gz", hash = "sha256:d376e3a649ff4733d1586488d58bf9305c2a60e9360a7d6c315a4639799aedd4"}, +] + +[package.dependencies] +aenum = "*" +boto3 = ">=1.35,<2.0" +botocore = ">=1.35,<2.0" +python-dateutil = "*" +requests = ">=2.32,<3.0" + +[[package]] +name = "jmespath" +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"}, +] + +[[package]] +name = "nexus-rpc" +version = "1.2.0" +description = "Nexus Python SDK" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "nexus_rpc-1.2.0-py3-none-any.whl", hash = "sha256:977876f3af811ad1a09b2961d3d1ac9233bda43ff0febbb0c9906483b9d9f8a3"}, + {file = "nexus_rpc-1.2.0.tar.gz", hash = "sha256:b4ddaffa4d3996aaeadf49b80dfcdfbca48fe4cb616defaf3b3c5c2c8fc61890"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.2" + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "promise" +version = "2.3" +description = "Promises/A+ implementation for Python" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"}, +] + +[package.dependencies] +six = "*" + +[package.extras] +test = ["coveralls", "futures", "mock", "pytest (>=2.7.3)", "pytest-benchmark", "pytest-cov"] + +[[package]] +name = "protobuf" +version = "6.33.1" +description = "" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b"}, + {file = "protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed"}, + {file = "protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490"}, + {file = "protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178"}, + {file = "protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53"}, + {file = "protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1"}, + {file = "protobuf-6.33.1-cp39-cp39-win32.whl", hash = "sha256:023af8449482fa884d88b4563d85e83accab54138ae098924a985bcbb734a213"}, + {file = "protobuf-6.33.1-cp39-cp39-win_amd64.whl", hash = "sha256:df051de4fd7e5e4371334e234c62ba43763f15ab605579e04c7008c05735cd82"}, + {file = "protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa"}, + {file = "protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b"}, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02"}, +] + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pyjwt" +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"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "python-dateutil" +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"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.2.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, + {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "s3transfer" +version = "0.15.0" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "s3transfer-0.15.0-py3-none-any.whl", hash = "sha256:6f8bf5caa31a0865c4081186689db1b2534cef721d104eb26101de4b9d6a5852"}, + {file = "s3transfer-0.15.0.tar.gz", hash = "sha256:d36fac8d0e3603eff9b5bfa4282c7ce6feb0301a633566153cbd0b93d11d8379"}, +] + +[package.dependencies] +botocore = ">=1.37.4,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] + +[[package]] +name = "sentry-sdk" +version = "2.47.0" +description = "Python client for Sentry (https://sentry.io)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "sentry_sdk-2.47.0-py2.py3-none-any.whl", hash = "sha256:d72f8c61025b7d1d9e52510d03a6247b280094a327dd900d987717a4fce93412"}, + {file = "sentry_sdk-2.47.0.tar.gz", hash = "sha256:8218891d5e41b4ea8d61d2aed62ed10c80e39d9f2959d6f939efbf056857e050"}, +] + +[package.dependencies] +certifi = "*" +urllib3 = ">=1.26.11" + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +anthropic = ["anthropic (>=0.16)"] +arq = ["arq (>=0.23)"] +asyncpg = ["asyncpg (>=0.23)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +celery-redbeat = ["celery-redbeat (>=2)"] +chalice = ["chalice (>=1.16.0)"] +clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] +google-genai = ["google-genai (>=1.29.0)"] +grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"] +http2 = ["httpcore[http2] (==1.*)"] +httpx = ["httpx (>=0.16.0)"] +huey = ["huey (>=2)"] +huggingface-hub = ["huggingface_hub (>=0.22)"] +langchain = ["langchain (>=0.0.210)"] +langgraph = ["langgraph (>=0.6.6)"] +launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"] +litellm = ["litellm (>=1.77.5)"] +litestar = ["litestar (>=2.0.0)"] +loguru = ["loguru (>=0.5)"] +mcp = ["mcp (>=1.15.0)"] +openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] +openfeature = ["openfeature-sdk (>=0.7.1)"] +opentelemetry = ["opentelemetry-distro (>=0.35b0)"] +opentelemetry-experimental = ["opentelemetry-distro"] +opentelemetry-otlp = ["opentelemetry-distro[otlp] (>=0.35b0)"] +pure-eval = ["asttokens", "executing", "pure_eval"] +pydantic-ai = ["pydantic-ai (>=1.0.0)"] +pymongo = ["pymongo (>=3.1)"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +starlite = ["starlite (>=1.48)"] +statsig = ["statsig (>=0.55.3)"] +tornado = ["tornado (>=6)"] +unleash = ["UnleashClient (>=6.0.1)"] + +[[package]] +name = "six" +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"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sqlparse" +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"}, +] + +[package.extras] +dev = ["build", "hatch"] +doc = ["sphinx"] + +[[package]] +name = "temporalio" +version = "1.20.0" +description = "Temporal.io Python SDK" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "temporalio-1.20.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:fba70314b4068f8b1994bddfa0e2ad742483f0ae714d2ef52e63013ccfd7042e"}, + {file = "temporalio-1.20.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffc5bb6cabc6ae67f0bfba44de6a9c121603134ae18784a2ff3a7f230ad99080"}, + {file = "temporalio-1.20.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1e80c1e4cdf88fa8277177f563edc91466fe4dc13c0322f26e55c76b6a219e6"}, + {file = "temporalio-1.20.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba92d909188930860c9d89ca6d7a753bc5a67e4e9eac6cea351477c967355eed"}, + {file = "temporalio-1.20.0-cp310-abi3-win_amd64.whl", hash = "sha256:eacfd571b653e0a0f4aa6593f4d06fc628797898f0900d400e833a1f40cad03a"}, + {file = "temporalio-1.20.0.tar.gz", hash = "sha256:5a6a85b7d298b7359bffa30025f7deac83c74ac095a4c6952fbf06c249a2a67c"}, +] + +[package.dependencies] +nexus-rpc = "1.2.0" +protobuf = ">=3.20,<7.0.0" +types-protobuf = ">=3.20" +typing-extensions = ">=4.2.0,<5" + +[package.extras] +grpc = ["grpcio (>=1.48.2,<2)"] +openai-agents = ["mcp (>=1.9.4,<2)", "openai-agents (>=0.3,<0.5)"] +opentelemetry = ["opentelemetry-api (>=1.11.1,<2)", "opentelemetry-sdk (>=1.11.1,<2)"] +pydantic = ["pydantic (>=2.0.0,<3)"] + +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + +[[package]] +name = "types-protobuf" +version = "6.32.1.20251105" +description = "Typing stubs for protobuf" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "types_protobuf-6.32.1.20251105-py3-none-any.whl", hash = "sha256:a15109d38f7cfefd2539ef86d3f93a6a41c7cad53924f8aa1a51eaddbb72a660"}, + {file = "types_protobuf-6.32.1.20251105.tar.gz", hash = "sha256:641002611ff87dd9fedc38a39a29cacb9907ae5ce61489b53e99ca2074bef764"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "tzdata" +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 = "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\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "whitenoise" +version = "6.11.0" +description = "Radically simplified static file serving for WSGI applications" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "whitenoise-6.11.0-py3-none-any.whl", hash = "sha256:b2aeb45950597236f53b5342b3121c5de69c8da0109362aee506ce88e022d258"}, + {file = "whitenoise-6.11.0.tar.gz", hash = "sha256:0f5bfce6061ae6611cd9396a8231e088722e4fc67bc13a111be74c738d99375f"}, +] + +[package.extras] +brotli = ["brotli"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.11" +content-hash = "33bcae3d5a20ac5a5cbabf6a11f0b74a9cfa86ef799f7df5ead9052178fefb07" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d812da7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "teams" +version = "0.1.0" +description = "" +authors = [ + {name = "Ruslan Bakiev",email = "572431+veikab@users.noreply.github.com"} +] +readme = "README.md" +requires-python = "^3.11" +dependencies = [ + "django (>=5.2.8,<6.0)", + "graphene-django (>=3.2.3,<4.0.0)", + "django-cors-headers (>=4.9.0,<5.0.0)", + "psycopg2-binary (>=2.9.11,<3.0.0)", + "requests (>=2.32.5,<3.0.0)", + "temporalio (>=1.4.0,<2.0.0)", + "python-dotenv (>=1.2.1,<2.0.0)", + "pyjwt (>=2.10.1,<3.0.0)", + "cryptography (>=46.0.3,<47.0.0)", + "infisicalsdk (>=1.0.12,<2.0.0)", + "gunicorn (>=23.0.0,<24.0.0)", + "whitenoise (>=6.7.0,<7.0.0)", + "sentry-sdk (>=2.47.0,<3.0.0)" +] + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/teams/__init__.py b/teams/__init__.py new file mode 100644 index 0000000..baaa45d --- /dev/null +++ b/teams/__init__.py @@ -0,0 +1 @@ +# Django orders service \ No newline at end of file diff --git a/teams/__pycache__/__init__.cpython-313.pyc b/teams/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5032b0b13c8f43fdf5d182552f443f068435019 GIT binary patch literal 167 zcmey&%ge<81nR2Nnc_hDF^B^Lj8MjB79e9PLpp;dqu)w~A|@d3Gf3)|n|^3UXlLdVwiu?iTY=05pVyYXP7@Sh(VedgX!26)693_t|3ZmW7>Hg z)-`BdOh0eHh6b&V8Rt#d)Sxvn7PBC0h?(at*fPYV3?t0-mU@^myjMN6Jt89<%V66n ztER@P{ghRyCN<5XKhrTA+q)gDu>lqcmt^q+G^k9{fMNjpLB z1<*T?8sKlX_o~#%7=Y|8RiZtP1Gxy^T9_eb@uTLL>H;hVuWOo8{N1*+N~v+?KU6vG z@gyD|2Ad*n8I>AgkPG!3MPc+*DL3j>rBL6k`{4WeQ)NBKtIFcB)R-E|H29HX`33dt z_6XjNr7Z324Qfz7@|~)G0G(0wpDJ?}4XQGzKXo3Ro1s-R6x85xGz{O1K?aRH&|p7| zqcBdO^Y{`PgP8+efbaMN&6EaR4Cp}iGVGpUO1L#Mo8(`~+QmPdR4f3Ku zz%Zr3(LheB`~kX#@1TBs7rh5KeF*CzbRA-M15Klw zQxw4&bOWO42HvfJ7Uu5QM-*kP@{&!r%K&HcaNkZr%HW@hL5DUQ>8+x&v^D! znR_UFq-kzV;qbM2NszL{)*@vwgGE>? z8-gV_#w!-%{5|a$_qt+f|x=56ERbw`n=+&#dJO^ zwAQisSj1vJBg(|NC5g{e{#H7-DFPt#q4-=vX!;_=xJTvDthhDV+&uYI+@8#=17zZ) zl%J$NY-Sp$=_A~|36v?jvyGGrWD5Bb zmR1Al?1q#t_Z;VLme$Jy$Cz{r#Iv+r6y>t#*ka4mcNaDQg7g~f@EzaLFzB(AgHT&C z1uU8yxl(ha#@To?1A(A+`TMuG|HV$si*$2vp<8&Wo#6Q4oa1;U_V@n~9}Y@OC2 z0ZFonrP6i)wTKS!i4N2{wyaPr;}pvd+4qh+(hRcPX$@-fI(GTrH7HZq*eK+|aytAE zQ5ioDgyHiLYNA7Q`j{<{b?cpWgkTOagT_MZpkw24C)6-?ZeDDLu4C7Y6P(CNnWs;h z=^glJ7N&;(3B*igWqeG2tT&PuQTY z`E|s2FBFb(@d(jHqC6s|(9)97pp1#7Bs3gU?rXyJOkR@$koACYv-!J{Txbyqkx(*p zk9H{9{f(^_Q9j$UCcD~3vI`U1bE~^P9*wFE-lOD-dtgA4Vq&Bl`z~S_u)3kiMVWU`O^xGUrS8T+Ub%^ z^f_@$%n=qcqL^NTB%-iFpQFu|c(an6UWM)|ek#c&P%O{`U=iPG9N(G|`$A$anGohz zVu&JH9##!*6j9siU_r`ef=`}3m5H^vC5q58$CuCiw++PIlx>DAv>bt(Im{7HOG89R zCLjrfqXlOol1K>Ac#`AeaJf^1*h02YxOH>t#*F_y=_<&?NN>-aw2HOV5i67vOahH3 z1=dlPA=`LRVEWu7Hc5R`2y*r*Hc*}!Zj}<=rVKLJrURXKSE~-Ok!~=y?j&r28D{B+ z{%pnc7b?-Ni6v~Ml%e@5AUcX2v2W%j8jW;LXw3Bhxs=E7ssx42yBe^}L3CQzyG*Y; z`ks&l$krm{he!-fj{0(V*mwZ2O=e8oCMH3kmxmzG3l#Ec3$_3h;2$wHssn0@7{EgC zfL`|W^+R6_*hB62QCoCUUMdk=Xkj6-!bJp{{)o84h@Tab{4z=cFTqElVi2*zCkcYU zf_g>7ycmr{7PyrV&k-x1NF-^*!io4SVUyhA(n2W7A>vp;b5Sb401q}=+7O^s0m9a> zR2H-F(4y}lLr#XvxCjqKxUTciL5ZmdmGP{Avyk*q!wsIUSimKs1yRDPEdUYQ0=$Tp zLvx%EPDD63J-WyVA19#5O;FxK^vX>*l~T!%0fi)(5pMJiON*wq6IXLh zj5c%P9sGMxOB;?n4drsE)o3(7G3+bG`-&NU#SFe;F8{^`Xu->CIXSD7Bgcjm7Xf10R{-KqKRRvC}t>EA2wma8K-Yo3`Z z<5nE5J@c-4&%SGa`P=Ueb;oqY_A@(B9lly)uT=~RWB9`IJIg-ft1-TPW~jytDfVH- z^o%RRV~TI^(874H9Wn;5 zp{jEq+L@lweb>Yfu8A)V%J}5|_)Kkl=F6_<=C8~8Z z9*BVZjn3lgs@UK7%=WH|`OwQa&Qz?2CdSQaK&j&P9$1IoF#1vZq1Na&zhOlw`bY4>^nwkj*)M! z)g9xM)4kiRxP5zx-Gn+jKk;qs+t{vlXLc{P8~edA^gaLDW^}-5Y~wU9A8NbY=0mN{X*jepY`@Y!^3tO8o&m^S8+8M)VsIE`J%u`S#b_F9)msxqA2gij^kp_KWT6@N{(s)jdf#`2cd-|7U;o g{0DX49mr`gt=B`tQyskW{rh$I-5TS>6)2$p03vG9^#A|> literal 0 HcmV?d00001 diff --git a/teams/__pycache__/settings_local.cpython-313.pyc b/teams/__pycache__/settings_local.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8e56bce8c634046aa42ace4495542ecd724d2b0 GIT binary patch literal 2005 zcma)7OK;mo5GM7cL{XL%Sys{n%>#YNtw|e6+9C)7&=TdUkwr&KHEm#kpry6Um=c*? z%Bgwcy_Z}H1K7vRH!NarGF$9zW-D@^J!Q101 z0z^>~#3UAR5(DB-Sd^mg1bwGL67e8FqaeW2PuLX}@M6HtfD90};vl=sGYps$BP%Qt zKn`WWJes2(bM!Y4@+eO^R{{>Y0t%=A7SIAHf<-BUij=X~E0wSQ6(n0`No><_cRj(f zJBGE~h5O5U@IWK%QaR-ny3dV4&lWrOz(wY^XmtDLz`HVw4ypYIGhko>2d;bIKDc|gJ)qeHum7N8v?EsN@~sJ9PEo&{$;F_}n9f6{-?R^Aht}_3F=jV$R{CksJsWBXMu^1|< zlT)XKYE1Hz!}NozJIL zUnTs)GF^DrOH(kE%Mp@Ln(OtZLSjvML-FEhxQvD*+SV!_C*jHTNQ6HXCS&B?2$QE? z`gRvuSX?uE^tjkH6I$Jxx3KHF&I3uBTnxK5b|2hbSy}P&0gNebJFdNN8X_7t%O#R@ z=q+NI+bBwJ6~gIy5JcBW5%?_1sWjPdJ||Aq5!QCAnA>Iwc1dg$}NRtTB@q)?N+0rHftoMDI1#&SyKQ> zRb@>Mf&d~kMkY&2P`R|1cO59J^P z$yb${+-_*P+}2dRR#zHTQmm*gpihzwR$Xn?Y4HImyvJ$IXXrGW?zpNnwYuB@KQr%w zE0{E-Hl|mZgofBM2HVD-3HPO!Hrm6E(Sc`mmd>U`nv7DoV(7Yb?yx@TlIR=|?jh~w z(1MT9=hS)duZ4f1%Q(ui?4L~OH-7FF_v52C{H-JY*021XH~i8Ozx0M*J>pk=?pV0) zr$=1IkB{aHuj0ON5=-YJe)go4%0~S3xX9$L`J2lDUM3g;i_`DSUY42qpLTLm?B!D+xiD{wUEF7 literal 0 HcmV?d00001 diff --git a/teams/__pycache__/urls.cpython-313.pyc b/teams/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2800b3e960c68d81f8f299a2f44a65cb2368d226 GIT binary patch literal 1350 zcmah|&rcIU6rOFj?Y3oGN=-ySK@6nzN7o=d03yMQ$u1aet1+fhx9!p{wB6;+7PL2S z9N}OHH%=zrOgx)#^jK&Nag)Y_CvO=30cWOd3L%Y?&CK_HeBaD_J98$AK14?O%aZ=p ziO_FqbVi^J&OUJneMCu=QJtP#m%ooD4e^UUq?tVP0{|9`MKl2(lNvvJZT+ zAN(xlQu<^OM26i;Kn_BXVUN-;hakkTpbW@i2#=w(a~vhTX)Y1z9H_e{Zg+79#T_lg zc)ql29HoWC-A=T-5>Fdc1^<)Ma)8CUB?#b*= zdy=8#z>HIJ!_`7KdgTu-D9YI`HDA^%1n{P670K$)VOY&*@3gXMMP0UFY1qoFZCQ5c zSv6bIb5B4ui_e!{>e`kae1SE%;7Dm|`Me{rJhgnzCi$Evy6S*2RAK+mvIl3G$yD(bKW=F>Myw!5WV6FXT{U+S7vU#j(WqC{V+d1;mR zNR<+}TYcdxYFT?49kc`aHMLSOVmYH?fu03&`;qN$JF8H_z*Emi+p|gff@66tXMk!M zfMc|s;G!u(x--tGrWs>dY%B1P5D0Yg(uqDZd$gO`Dh9_cbPfD8>-y!abFGBF9yyfN z?k|WmXXLU@x9LD%x6NbS(rliVYkO3jq30(*4oc+GSQ=FDXLLsLko{PqR{~2=#U-_p zRo8WGQ`$0M9h+)SlMK@`Hg#3Xs=0NolE)I;x;<0%nEBoo9+r)KwWK|Qn?#bnJ^YPK zr%sOJnrN(vMw)2)7t((qy@_U;D9*kIf8EIC-5%VD@6GPc?k(&tH27OTd7lk>-u^M_VFl{uPx+Za+CeD=S=8~eit sD<>0k#}jjhsd^%PH1Vc!ZKc6yD06tHc<}m&zke!vf{qV<7$p__1AuO9I{*Lx literal 0 HcmV?d00001 diff --git a/teams/settings.py b/teams/settings.py new file mode 100644 index 0000000..522c860 --- /dev/null +++ b/teams/settings.py @@ -0,0 +1,161 @@ +import os +from pathlib import Path +from urllib.parse import urlparse +from dotenv import load_dotenv +from infisical_sdk import InfisicalSDKClient +import sentry_sdk +from sentry_sdk.integrations.django import DjangoIntegration + +load_dotenv() + +INFISICAL_API_URL = os.environ["INFISICAL_API_URL"] +INFISICAL_CLIENT_ID = os.environ["INFISICAL_CLIENT_ID"] +INFISICAL_CLIENT_SECRET = os.environ["INFISICAL_CLIENT_SECRET"] +INFISICAL_PROJECT_ID = os.environ["INFISICAL_PROJECT_ID"] +INFISICAL_ENV = os.environ.get("INFISICAL_ENV", "prod") + +client = InfisicalSDKClient(host=INFISICAL_API_URL) +client.auth.universal_auth.login( + client_id=INFISICAL_CLIENT_ID, + client_secret=INFISICAL_CLIENT_SECRET, +) + +# Fetch secrets from /teams and /shared +for secret_path in ["/teams", "/shared"]: + secrets_response = client.secrets.list_secrets( + environment_slug=INFISICAL_ENV, + secret_path=secret_path, + project_id=INFISICAL_PROJECT_ID, + expand_secret_references=True, + view_secret_value=True, + ) + for secret in secrets_response.secrets: + os.environ[secret.secretKey] = secret.secretValue + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'dev-secret-key-change-in-production') + +DEBUG = os.getenv('DEBUG', 'False') == 'True' + +# Sentry/GlitchTip configuration +SENTRY_DSN = os.getenv('SENTRY_DSN', '') +if SENTRY_DSN: + sentry_sdk.init( + dsn=SENTRY_DSN, + integrations=[DjangoIntegration()], + auto_session_tracking=False, + traces_sample_rate=0.01, + release=os.getenv('RELEASE_VERSION', '1.0.0'), + environment=os.getenv('ENVIRONMENT', 'production'), + send_default_pii=False, + debug=DEBUG, + ) + +ALLOWED_HOSTS = ['*'] + +CSRF_TRUSTED_ORIGINS = ['https://teams.optovia.ru'] + +INSTALLED_APPS = [ + 'whitenoise.runserver_nostatic', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'corsheaders', + 'graphene_django', + 'teams_app', +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'teams.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'teams.wsgi.application' + +db_url = os.environ["TEAMS_DATABASE_URL"] +parsed = urlparse(db_url) +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': parsed.path.lstrip('/'), + 'USER': parsed.username, + 'PASSWORD': parsed.password, + 'HOST': parsed.hostname, + 'PORT': str(parsed.port) if parsed.port else '', + } +} + +# Internationalization +LANGUAGE_CODE = 'ru-ru' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +# Static files +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' + +# Default primary key field type +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# CORS +CORS_ALLOW_ALL_ORIGINS = False +CORS_ALLOWED_ORIGINS = ['https://optovia.ru'] +CORS_ALLOW_CREDENTIALS = True + +# Logging +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + 'django.request': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + }, +} + +# Logto JWT settings +LOGTO_JWKS_URL = os.getenv('LOGTO_JWKS_URL', 'https://auth.optovia.ru/oidc/jwks') +LOGTO_ISSUER = os.getenv('LOGTO_ISSUER', 'https://auth.optovia.ru/oidc') +LOGTO_TEAMS_AUDIENCE = os.getenv('LOGTO_TEAMS_AUDIENCE', 'https://teams.optovia.ru') +# ID Token audience can be omitted when we only need signature + issuer validation. +LOGTO_ID_TOKEN_AUDIENCE = os.getenv('LOGTO_ID_TOKEN_AUDIENCE') + +# Odoo connection (internal M2M) +ODOO_INTERNAL_URL = os.getenv('ODOO_INTERNAL_URL', 'odoo:8069') diff --git a/teams/settings_local.py b/teams/settings_local.py new file mode 100644 index 0000000..c5e2b7c --- /dev/null +++ b/teams/settings_local.py @@ -0,0 +1,71 @@ +# Local settings for makemigrations (no Infisical, SQLite) +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = 'local-dev-key' +DEBUG = True +ALLOWED_HOSTS = ['*'] + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'corsheaders', + 'graphene_django', + 'teams_app', +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'teams.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +LANGUAGE_CODE = 'ru-ru' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# CORS +CORS_ALLOW_ALL_ORIGINS = False +CORS_ALLOWED_ORIGINS = ['http://localhost:3000', 'https://optovia.ru'] +CORS_ALLOW_CREDENTIALS = True diff --git a/teams/urls.py b/teams/urls.py new file mode 100644 index 0000000..e019bb2 --- /dev/null +++ b/teams/urls.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from django.urls import path +from django.views.decorators.csrf import csrf_exempt +from teams_app.views import test_jwt, PublicGraphQLView, UserGraphQLView, TeamGraphQLView, M2MGraphQLView +from teams_app.schemas.public_schema import public_schema +from teams_app.schemas.user_schema import user_schema +from teams_app.schemas.team_schema import team_schema +from teams_app.schemas.m2m_schema import m2m_schema + +urlpatterns = [ + path('admin/', admin.site.urls), + path('graphql/public/', csrf_exempt(PublicGraphQLView.as_view(graphiql=True, schema=public_schema))), + path('graphql/user/', csrf_exempt(UserGraphQLView.as_view(graphiql=True, schema=user_schema))), + path('graphql/team/', csrf_exempt(TeamGraphQLView.as_view(graphiql=True, schema=team_schema))), + path('graphql/m2m/', csrf_exempt(M2MGraphQLView.as_view(graphiql=True, schema=m2m_schema))), + path('test-jwt/', test_jwt, name='test_jwt'), +] \ No newline at end of file diff --git a/teams/wsgi.py b/teams/wsgi.py new file mode 100644 index 0000000..66d0000 --- /dev/null +++ b/teams/wsgi.py @@ -0,0 +1,6 @@ +import os +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'teams.settings') + +application = get_wsgi_application() \ No newline at end of file diff --git a/teams_app/__init__.py b/teams_app/__init__.py new file mode 100644 index 0000000..6c1ab79 --- /dev/null +++ b/teams_app/__init__.py @@ -0,0 +1 @@ +# Orders Django app \ No newline at end of file diff --git a/teams_app/__pycache__/__init__.cpython-313.pyc b/teams_app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c442b4f2b3dd7845a56f098f7d603d218da82c91 GIT binary patch literal 171 zcmey&%ge<81nR2NnZiK&F^B^Lj8MjB79e9PLpp;dqu)w~A|@d3Gf3)|mwsq*YEiL% zQE72bVqQ{Wc4lgsetCXTc5y*sa;koQK}mjDW}<#lVsdtBUP`fkNory)l#EX-DA13O q&&R;fW^s!MMf<{6`>J#cqD)R$+&y1bK6r)H)%JqDlIllMYduQAmAI}T^ zyZeV7{*x8rZ%hV%nf{=6IwiydVGG+>5DkNjs<2abdLh+FQ@UZ2*|5lJWGK_fQnrz! zTvgmKzS%dR+4(#!Zlo_7dOj_V3)`F#w#C>3JD9=fVp@+b_7|uylG7aG%rIwmkDSGk zoYoL$jycEn$T>ceGc&}QXHLYlm)J`qIkQ8YMdrl!?}f8Gk~25NSz=CL_rh6eR%#PZ zk+GT~E!STT+fR}2T1J}J19w^GVHbntrGR7!<1gDQVd5rX)Ry~)=hvg#L0cBSjRX3c zqMKnWkOd&-eV>9j_WE_9KKeB!dM9}LK->^y*n(2FL1{Zhrky6sHYsCUl(jRIYi4UX zIi|_rq6rr*X?DAzFU_{Q97rp=(`Fxwt*DtqPDD+IxAGnZZW8#8o74$dtkWyiT0Qk=BHcF>Nn-3sHx@x!>&a#v+R%~-jFBN>kam%N|&K+y7IS>{Om zsNGr}EF5&47>V#WxP-!`ZW6?fLzC=ikN<_dQb#J{I(6fg+g@^ShrvqyPDHokj_U>W zsFOr1p<7>aJW^T$M}L@#gqMwdsiRrJ9cm3(E;z? z;-kq+_rIcJ1K=y?x{O@wvYS^$Szv0;ol2VtPdWJrqcpunQ-h(7^Ejp`UuEFmwlp6@npsHm9i(}4APvy3 zEX{LWr{z(loz*U%;%|{wuE_z8_Nb9v{*==zfoJu5(mH+i*uNFM&FuEi?8nrn!kS!YcWu;+!DF%J8hO1WWn{4 za3ydMgjOI;AMpmA!S2MaUELTux>}Z|0eTGv!O$TX_(exxsxS~@SF>Ek6b7EexK(hE ze9F-uxN?6{1F`+$n9)3=V;*`bDkzBlv!i3joxnb;ibj8^M?hv>{WK9uUlzTn+fHcJ z@ghGMNTHhIbDU5uX6o1DGb|`Fvn30g5(JR$rLB>r_MCob>7_+#atq5_MUl!XWPZaq1mxUWusL~Z zeex8Lb3h&ja&8A%+Axj+dG_~{n=@zDXU+h57RV})XSYgy&Oa+}Y#1jrw7Yqd9MS>)m8$HIOgg<9pxBd&!{Dm>_n2*?D^LPZ>u&UgB?rm2qTOh zlOu|BlPr_Zgn6rk`4{x7U*(~f1Yzyj1tB%#`$h3R&pI+`S8|1P6b#laxg!?@DI@4H ziazr&U_RDZfMZE595d6Wg7?&Woo9E?^+x=YXGz|STg_e`i&QlaWU^oC{7{NCHsjiP zfEUF*ISD_QIzYyI0>DfFSVNgQJ_YcrCNqc8nG&cXELF<07Vk>Yk1liUs)DyglovYhOCGg& zyDPF-MZYD-iGcY)PjY7KBe@DZ2b>Bx^#Q~LhLbgy#iu1kNE%q literal 0 HcmV?d00001 diff --git a/teams_app/__pycache__/auth.cpython-313.pyc b/teams_app/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93d5c1bf53d38c47515f88ff7703ee0b821dfbc3 GIT binary patch literal 3467 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 literal 0 HcmV?d00001 diff --git a/teams_app/__pycache__/models.cpython-313.pyc b/teams_app/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..793ceda6c8e2debd1e3a96a9e475751b38c320b6 GIT binary patch literal 9636 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 KevDu+nEe;v>yDQI literal 0 HcmV?d00001 diff --git a/teams_app/__pycache__/views.cpython-313.pyc b/teams_app/__pycache__/views.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5614d6e5500741622127035083e4a4b80fa36aee GIT binary patch literal 4957 zcmcf_TWlQF_0G)B?s)C>u3xdUPVmHyLpH=~z&H;Q5O!in)_K+AEwmdt8t;znN!ByV zomnSw5i)#G(^d@?LN#ioSnWrXzX*ZqLLcVwk!E`+>kH9Q{KgWL;gg723(vUViE!s0QXCQp_)XH1{1ZkHc?0GL~=SH2~pBl zK8S`uPR&q#B22?Be{d+0XrK*=M%tKYqD_fr+6?sCD4{KFq?1T>Q6hy-hx@pa0J_Pg z)&n(MMQwGd5ui3yQQKTFsGG-`So2L3cOu3(f~i`TrjMI|*Ck9tA6Ctrp_?lF zu1%YCOjggUlQ|21HXPNbl&qE+G0v*`Af*Pq$$*p?=S0R;OCD7fs#4j)Rj0{J8#&dK z$EYzW=adUsL&-Qb?smm8Xc{+q1I>fiTfK)xBiut$mDMEaE8{wos_`}?8IPJ|Bwq6S zNIj7PfD00jcrPc_#95sKNwwad6qvp=q@cum&#C)T^&WjV;$iD^CwZz_s@>1(e3Bb( zTBm!MRF`V@@Zn7!wmx@~DqZ;`KfHN;&BLTn_lB_39V&kmDNxuE8$#xKl*V%rM zRGP<_G4cr4LB^O3l6RqGsR>SRz86H^O)ji4?pn33p0+Lfx&L-rY*YS85NrR=ezWj= z;RXAz_Unb8+kX;OJ(Dvu-4g9L?6(USMf-1s8C(Rm8T&1JK}Bo^v9F4S z=Ya`W-T=4&vVKwcW!&La^y(Pv_)SZ(@+L*6(Ov-;j4?C>_=%e$z-cZXk87WN_0?Cv zTIV{voK^L4Yr+YYe&wh-rK#tf=3^(H7&$4A4E7(FXn3ka}pakgXhM42m^o*)oH_^Zt zrDq_BdmO~&lO*DFI=$>n<4-Zm5plk?7>UjD zYXaG}eSYxw!9{*Cy+{{>3(sEby_UC=Pua;IUi-UwMSr`` z?mDnAQfwNS6_y*@uQzsn(AYWOS7@Eax~;2(LEXdc^XLBDv>?6RR@gFdt+x<9`Jr&~%M}h6zg*klGIF5n z($*cb!pm(1A-cS!W1joX5%|?w5H^E=xg(dQ#iy=4vv|A^9{x}m{&FRNJH9Xvz)QVQ zJ8&QQ!vJ@vjl6qb@K6i)ZePox2=`u)0sOrPkFYIxxQ=_Tv*U1pd!Jzde?Pzj#Pab3 zRdN&GKlTGKMa<_k9e?on38O!6O&r%UnXGya%7wG(RDLw8rSG6OjDS5MXscKo&>?Ok z-Bv{~o48e^RLYZN7zi7O#5{bC5C!Z%&O&vlzEDk7i2idif==q&~2b6?E>J`44zM`IXpAQ(H%&B7(p)p6Kx17fI=l9POGe$@@-1W z?&0(xtnNYfGXQ4DCxQB(KKJ}{bDN5Rj-_z(3d{2?%i*Sr(?6S@yZ6%W*LGizb{ERw zRSfT)^({r3e=YnSc|E*$e&T9ZVaHxu*n4ZawizR>iW!Q#V8f zq6=z)mUd~e_#F`v25GA(@=@m1sy?d^C^&kUB&g|DXYU$hgiDgAp-f;eb3gP4hnYQ$ zNXE$(_Jk9htb{UHG7b_P-pqqe=o^}@S+ab^=lD!DJLdQlI&M1tvk><%*T`}@tC%LZ z5XK%@e8x37J&Z!4cf3Q~lQ#(KCVJ7W8L}+yDT@2-j{UR#*=GyFmSrLIQs4ujZEm|Q z#O8Mth1e=Ek}KqjfC}l#q$_RmP>KK_-o!TZ){EpL zU;pPmmfy8X0MH?imNdZ=BKWW@8TA18}3wbisP_rwc->H6vbY<_PhObUj z2wdm?dUIfXqX_Ut*3I$QMspy`w`Pud=vJz&@gopE{oEba04{Ooq>`S1jL~fw-DNaZ z6-)8piH(U_#D-X-YDVl9A^tMr=mjjrD%F*&2=O?jficCMBkvCYTrG-X<#!t0wP&*OdX1#+>tdOtZuGcm4_g_xI=Csv zb#(X6)Qt!?Hr!rFI9RG$kJ2pc!17|A2LO!;!+cC?KPJH&M7%+^-yjd&Alv>y1`1?g z#ZTDKr))bTF{>P)D=zxkV+>*9pZj=*T_phi9jRej*4lVx*Iexy5O%Fa0K9zmE_eXw Lsm};ZZq)t dict: + """Decode and verify a JWT, enforcing issuer and optional audience.""" + try: + signing_key = self._jwks_client.get_signing_key_from_jwt(token) + header_alg = jwt.get_unverified_header(token).get("alg") + + return jwt.decode( + token, + signing_key.key, + algorithms=[header_alg] if header_alg else None, + issuer=self._issuer, + audience=audience, + options={"verify_aud": audience is not None}, + ) + except InvalidTokenError as exc: + logger.warning("Failed to validate Logto token: %s", exc) + raise + + +def get_bearer_token(request) -> str: + """Extract Bearer token from Authorization header.""" + auth_header = request.META.get("HTTP_AUTHORIZATION", "") + if not auth_header.startswith("Bearer "): + raise InvalidTokenError("Missing Bearer token") + + token = auth_header.split(" ", 1)[1] + if not token or token == "undefined": + raise InvalidTokenError("Empty Bearer token") + + return token + + +def scopes_from_payload(payload: dict) -> list[str]: + """Split scope string (if present) into a list.""" + scope_value = payload.get("scope") + if not scope_value: + return [] + if isinstance(scope_value, str): + return scope_value.split() + if isinstance(scope_value, Iterable): + return list(scope_value) + return [] + + +validator = LogtoTokenValidator( + getattr(settings, "LOGTO_JWKS_URL", "https://auth.optovia.ru/oidc/jwks"), + getattr(settings, "LOGTO_ISSUER", "https://auth.optovia.ru/oidc"), +) diff --git a/teams_app/graphql_middleware.py b/teams_app/graphql_middleware.py new file mode 100644 index 0000000..f1ce978 --- /dev/null +++ b/teams_app/graphql_middleware.py @@ -0,0 +1,81 @@ +""" +GraphQL middleware for JWT authentication. + +Each class is bound to a specific GraphQL endpoint (public/user/team/m2m). +""" +import logging +from django.conf import settings +from graphql import GraphQLError +from jwt import InvalidTokenError + +from .auth import get_bearer_token, scopes_from_payload, validator + +logger = logging.getLogger(__name__) + + +def _is_introspection(info) -> bool: + """Возвращает True для любых introspection резолвов.""" + field = getattr(info, "field_name", "") + parent = getattr(getattr(info, "parent_type", None), "name", "") + return field.startswith("__") or parent.startswith("__") + + +class PublicNoAuthMiddleware: + """Public endpoint - no authentication required.""" + + def resolve(self, next, root, info, **kwargs): + return next(root, info, **kwargs) + + +class UserJWTMiddleware: + """User endpoint - requires ID token.""" + + def resolve(self, next, root, info, **kwargs): + request = info.context + if _is_introspection(info): + return next(root, info, **kwargs) + + # Only process auth once (check if already processed) + if not hasattr(request, 'user_id'): + try: + token = get_bearer_token(request) + payload = validator.decode(token) + request.user_id = payload.get('sub') + logger.info(f"[UserJWTMiddleware] user_id set to: {request.user_id}") + except InvalidTokenError as exc: + logger.warning(f"[UserJWTMiddleware] Token error: {exc}") + raise GraphQLError("Unauthorized") from exc + + return next(root, info, **kwargs) + + +class TeamJWTMiddleware: + """Team endpoint - requires Access token for teams audience.""" + + def resolve(self, next, root, info, **kwargs): + request = info.context + if _is_introspection(info): + return next(root, info, **kwargs) + + try: + token = get_bearer_token(request) + payload = validator.decode( + token, + audience=getattr(settings, 'LOGTO_TEAMS_AUDIENCE', None), + ) + request.user_id = payload.get('sub') + request.team_uuid = payload.get('team_uuid') + request.scopes = scopes_from_payload(payload) + if not request.team_uuid or 'teams:member' not in request.scopes: + raise GraphQLError("Unauthorized") + except InvalidTokenError as exc: + raise GraphQLError("Unauthorized") from exc + + return next(root, info, **kwargs) + + +class M2MNoAuthMiddleware: + """M2M endpoint - internal services only, no auth for now.""" + + def resolve(self, next, root, info, **kwargs): + return next(root, info, **kwargs) diff --git a/teams_app/middleware.py b/teams_app/middleware.py new file mode 100644 index 0000000..62357f3 --- /dev/null +++ b/teams_app/middleware.py @@ -0,0 +1,56 @@ +import json +import logging + +from django.conf import settings +from django.utils.deprecation import MiddlewareMixin +from jwt import InvalidTokenError + +from .auth import get_bearer_token, scopes_from_payload, validator + +logger = logging.getLogger(__name__) + +class LogtoJWTMiddleware(MiddlewareMixin): + """ + JWT middleware для проверки токенов от Logto + """ + + def __init__(self, get_response=None): + super().__init__(get_response) + + # Audience validated only for non-introspection API calls + self.audience = getattr(settings, "LOGTO_TEAMS_AUDIENCE", None) + + def _is_introspection_query(self, request): + """Проверяет, является ли запрос introspection (для GraphQL codegen)""" + if request.method != 'POST': + return False + try: + body = json.loads(request.body.decode('utf-8')) + query = body.get('query', '') + return '__schema' in query or '__type' in query + except Exception: + return False + + def process_request(self, request): + """Обрабатывает JWT токен из заголовка Authorization""" + + # Пропускаем проверку для admin панели и статики + if request.path.startswith('/admin/') or request.path.startswith('/static/'): + return None + + # Пропускаем introspection запросы (для GraphQL codegen) + if self._is_introspection_query(request): + return None + + try: + token = get_bearer_token(request) + payload = validator.decode(token, audience=self.audience) + + request.user_id = payload.get('sub') + request.team_uuid = payload.get('team_uuid') + request.scopes = scopes_from_payload(payload) + + except InvalidTokenError: + return None + + return None diff --git a/teams_app/migrations/0001_initial.py b/teams_app/migrations/0001_initial.py new file mode 100644 index 0000000..c275c55 --- /dev/null +++ b/teams_app/migrations/0001_initial.py @@ -0,0 +1,68 @@ +# Generated by Django 5.2.8 on 2025-12-04 08:11 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Team', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.CharField(default=uuid.uuid4, max_length=100, unique=True)), + ('name', models.CharField(max_length=255)), + ('logto_org_id', models.CharField(blank=True, max_length=255, null=True)), + ('status', models.CharField(choices=[('PENDING_KYC', 'Требуется KYC'), ('KYC_IN_REVIEW', 'KYC на рассмотрении'), ('KYC_APPROVED', 'KYC одобрен'), ('KYC_REJECTED', 'KYC отклонен'), ('SUSPENDED', 'Заблокировано')], default='PENDING_KYC', max_length=50)), + ('prefect_flow_run_id', models.CharField(blank=True, max_length=255, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owned_teams', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'teams_team', + }, + ), + migrations.CreateModel( + name='TeamInvitation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.CharField(default=uuid.uuid4, max_length=100, unique=True)), + ('email', models.EmailField(max_length=254)), + ('role', models.CharField(choices=[('OWNER', 'Владелец'), ('ADMIN', 'Администратор'), ('MANAGER', 'Менеджер'), ('MEMBER', 'Участник')], default='MEMBER', max_length=50)), + ('status', models.CharField(choices=[('PENDING', 'Ожидает ответа'), ('ACCEPTED', 'Принято'), ('DECLINED', 'Отклонено'), ('EXPIRED', 'Истекло')], default='PENDING', max_length=50)), + ('invited_by', models.CharField(max_length=255)), + ('expires_at', models.DateTimeField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='teams_app.team')), + ], + options={ + 'db_table': 'teams_invitation', + 'unique_together': {('team', 'email')}, + }, + ), + migrations.CreateModel( + name='TeamMember', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.CharField(default=uuid.uuid4, max_length=100, unique=True)), + ('role', models.CharField(choices=[('OWNER', 'Владелец'), ('ADMIN', 'Администратор'), ('MANAGER', 'Менеджер'), ('MEMBER', 'Участник')], default='MEMBER', max_length=50)), + ('joined_at', models.DateTimeField(auto_now_add=True)), + ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='members', to='teams_app.team')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='team_memberships', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'teams_member', + 'unique_together': {('team', 'user')}, + }, + ), + ] diff --git a/teams_app/migrations/0002_add_user_profile.py b/teams_app/migrations/0002_add_user_profile.py new file mode 100644 index 0000000..49e3cff --- /dev/null +++ b/teams_app/migrations/0002_add_user_profile.py @@ -0,0 +1,30 @@ +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams_app', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('logto_id', models.CharField(max_length=255, unique=True)), + ('avatar_id', models.CharField(blank=True, max_length=100, null=True)), + ('phone', models.CharField(blank=True, max_length=20, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('active_team', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='active_profiles', to='teams_app.team')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'teams_user_profile', + }, + ), + ] diff --git a/teams_app/migrations/0003_add_team_type.py b/teams_app/migrations/0003_add_team_type.py new file mode 100644 index 0000000..c21f6f1 --- /dev/null +++ b/teams_app/migrations/0003_add_team_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2025-12-08 09:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams_app', '0002_add_user_profile'), + ] + + operations = [ + migrations.AddField( + model_name='team', + name='team_type', + field=models.CharField(choices=[('BUYER', 'Покупатель'), ('SELLER', 'Продавец')], default='BUYER', max_length=20), + ), + ] diff --git a/teams_app/migrations/0004_teamaddress.py b/teams_app/migrations/0004_teamaddress.py new file mode 100644 index 0000000..153d876 --- /dev/null +++ b/teams_app/migrations/0004_teamaddress.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.8 on 2025-12-09 03:18 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams_app', '0003_add_team_type'), + ] + + operations = [ + migrations.CreateModel( + name='TeamAddress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.CharField(default=uuid.uuid4, max_length=100, unique=True)), + ('name', models.CharField(max_length=255)), + ('address', models.TextField()), + ('latitude', models.FloatField(blank=True, null=True)), + ('longitude', models.FloatField(blank=True, null=True)), + ('is_default', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='teams_app.team')), + ], + options={ + 'db_table': 'teams_address', + }, + ), + ] diff --git a/teams_app/migrations/0005_remove_team_prefect_flow_run_id.py b/teams_app/migrations/0005_remove_team_prefect_flow_run_id.py new file mode 100644 index 0000000..4989a8b --- /dev/null +++ b/teams_app/migrations/0005_remove_team_prefect_flow_run_id.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.8 on 2025-12-16 01:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams_app', '0004_teamaddress'), + ] + + operations = [ + migrations.RemoveField( + model_name='team', + name='prefect_flow_run_id', + ), + ] diff --git a/teams_app/migrations/0006_add_address_status_and_selected_location.py b/teams_app/migrations/0006_add_address_status_and_selected_location.py new file mode 100644 index 0000000..9b83b1d --- /dev/null +++ b/teams_app/migrations/0006_add_address_status_and_selected_location.py @@ -0,0 +1,59 @@ +# Generated manually + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams_app', '0005_remove_team_prefect_flow_run_id'), + ] + + operations = [ + # TeamAddress fields + migrations.AddField( + model_name='teamaddress', + name='status', + field=models.CharField( + choices=[ + ('pending', 'Ожидает обработки'), + ('processing', 'Обрабатывается'), + ('ready', 'Готов'), + ('error', 'Ошибка'), + ], + default='pending', + max_length=20, + ), + ), + migrations.AddField( + model_name='teamaddress', + name='graph_node_id', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='teamaddress', + name='processed_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='teamaddress', + name='error_message', + field=models.TextField(blank=True, null=True), + ), + # Team selected location fields + migrations.AddField( + model_name='team', + name='selected_location_type', + field=models.CharField( + blank=True, + choices=[('address', 'Адрес'), ('hub', 'Хаб')], + max_length=20, + null=True, + ), + ), + migrations.AddField( + model_name='team', + name='selected_location_uuid', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/teams_app/migrations/0007_teamaddress_country_code.py b/teams_app/migrations/0007_teamaddress_country_code.py new file mode 100644 index 0000000..a015a37 --- /dev/null +++ b/teams_app/migrations/0007_teamaddress_country_code.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2025-12-16 12:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams_app', '0006_add_address_status_and_selected_location'), + ] + + operations = [ + migrations.AddField( + model_name='teamaddress', + name='country_code', + field=models.CharField(blank=True, max_length=2, null=True), + ), + ] diff --git a/teams_app/migrations/0008_remove_graph_node_id_and_change_default_status.py b/teams_app/migrations/0008_remove_graph_node_id_and_change_default_status.py new file mode 100644 index 0000000..6b3a688 --- /dev/null +++ b/teams_app/migrations/0008_remove_graph_node_id_and_change_default_status.py @@ -0,0 +1,31 @@ +# Generated manually + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams_app', '0007_teamaddress_country_code'), + ] + + operations = [ + migrations.RemoveField( + model_name='teamaddress', + name='graph_node_id', + ), + migrations.AlterField( + model_name='teamaddress', + name='status', + field=models.CharField( + choices=[ + ('pending', 'Ожидает обработки'), + ('processing', 'Обрабатывается'), + ('ready', 'Готов'), + ('error', 'Ошибка') + ], + default='processing', + max_length=20 + ), + ), + ] diff --git a/teams_app/migrations/0009_alter_teamaddress_status.py b/teams_app/migrations/0009_alter_teamaddress_status.py new file mode 100644 index 0000000..e78c897 --- /dev/null +++ b/teams_app/migrations/0009_alter_teamaddress_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2025-12-30 02:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams_app', '0008_remove_graph_node_id_and_change_default_status'), + ] + + operations = [ + migrations.AlterField( + model_name='teamaddress', + name='status', + field=models.CharField(choices=[('pending', 'Ожидает обработки'), ('active', 'Активен'), ('error', 'Ошибка')], default='pending', max_length=20), + ), + ] diff --git a/teams_app/migrations/0010_remove_team_status.py b/teams_app/migrations/0010_remove_team_status.py new file mode 100644 index 0000000..4c97a8e --- /dev/null +++ b/teams_app/migrations/0010_remove_team_status.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.8 on 2025-12-30 03:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams_app', '0009_alter_teamaddress_status'), + ] + + operations = [ + migrations.RemoveField( + model_name='team', + name='status', + ), + ] diff --git a/teams_app/migrations/0011_teaminvitationtoken.py b/teams_app/migrations/0011_teaminvitationtoken.py new file mode 100644 index 0000000..81e9409 --- /dev/null +++ b/teams_app/migrations/0011_teaminvitationtoken.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.8 on 2025-12-30 07:43 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams_app', '0010_remove_team_status'), + ] + + operations = [ + migrations.CreateModel( + name='TeamInvitationToken', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.CharField(default=uuid.uuid4, max_length=100, unique=True)), + ('token_hash', models.CharField(max_length=255, unique=True)), + ('workflow_status', models.CharField(choices=[('pending', 'Ожидает обработки'), ('active', 'Активен'), ('error', 'Ошибка')], default='pending', max_length=20)), + ('expires_at', models.DateTimeField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('invitation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to='teams_app.teaminvitation')), + ], + options={ + 'db_table': 'teams_invitation_token', + }, + ), + ] diff --git a/teams_app/migrations/0012_add_selected_location_details.py b/teams_app/migrations/0012_add_selected_location_details.py new file mode 100644 index 0000000..cc2dc81 --- /dev/null +++ b/teams_app/migrations/0012_add_selected_location_details.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.8 on 2026-01-03 05:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams_app', '0011_teaminvitationtoken'), + ] + + operations = [ + migrations.AddField( + model_name='team', + name='selected_location_latitude', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='team', + name='selected_location_longitude', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='team', + name='selected_location_name', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/teams_app/migrations/__init__.py b/teams_app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/teams_app/migrations/__pycache__/0001_initial.cpython-313.pyc b/teams_app/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ccbe3a04f6ca52701d590f7b1f875b575cfb2a4 GIT binary patch literal 4448 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 literal 0 HcmV?d00001 diff --git a/teams_app/migrations/__pycache__/0004_teamaddress.cpython-313.pyc b/teams_app/migrations/__pycache__/0004_teamaddress.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3aa9c136d404b245d4f17cb6ca40b1dcb5845edb GIT binary patch literal 1964 zcma)7%}*Og6rWw&Ywu#nnr{=Z2|`s&krxH5im0l}m(xN+C6*wOQjON^;A~lTo!MOy za;j9R=UgK3FZ4!E;h1BNwtFy|9$KjnD%RBSl`^}r*`oFReM5Aa5>5(y{NB5#z z$@bc7Lm|=J*T!2Y@OTF%_H3wS^ev%&76N}LWQI`SN-wn5?Yz^Pk>k6#yS&YESHi;?0l(SG5xka&NHnk`h=dD6ZGt21gbuyPtxJ? zo!k_YA9niZG64>tqhy~LM;X79Mqc(_anDkcx3WlMySfsI9Z9+=$TnRqYy zJ|rv8XAp(#Id z&q`RFhy0eHQLafYTjeDvTajPzN#hEsN#XqF%WJT-7zvG~f8zOuBs#uT?gIycx5#-0z= z+=s;b)G7r&@i3+z1Dh-OBlfNK2ztrWXy8GiOU9u^jsA%dI!>XzicdQiI-J2>ZHS&3xsAO0Y-x;%C8|6A=0lH6+w@v`v8vv`NR6Eq8{@O}@!6lp^Ho*5NZmf&X-wqn6Zy04x6#_fT2(Dx zq{dGV8X2vg(ay}bgSAYtsy?_#O`IM#viIuQd*_8(cB!f^w^j4?>^z{d*HBz_zFo^M zSJjm^jhT9S=4|K9?(5w~VXanJYZNwXh0R7`r&idhl1dYyW1%R%M`-MU2tw)$u{qQ; z*w-9GK=EtH%TV9@iIl7~BLn@?59{>vP6~5zmTpx(Z)-qjIM$RwL#t#yqXa~5UwO7-1!Cd{EF`WA*BWBrP5HRYwGl$ I2!znb->nh;lmGw# literal 0 HcmV?d00001 diff --git a/teams_app/migrations/__pycache__/0005_remove_team_prefect_flow_run_id.cpython-313.pyc b/teams_app/migrations/__pycache__/0005_remove_team_prefect_flow_run_id.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c618b35801731540c14bc81ce20d44234931d91d GIT binary patch literal 723 zcmZuvzi-qq7`5}ON!k+!1qui%KowFzYHpwd2z6vA<%WycPWt3a+*7#x>gU{5Y)DAV zEd3+c_*YsQhbJn2HS1R#9? zC7EVi;SGS}Da*7>v1jR#bGyXc|St9+i;#cX& zTL7X~NyWhG8q?9V<^lja%Cyjzm&f5d!BK=b6&Wi=Y?AU>cv9j-R4nFUS=Hq%W#Nd$ z6V+3MO#*GzIfSd12vs6`fXzXgVBkMd>{cI8zfx0ls^Xa{pUK=I-qX2h+;l!3&VAx- zp1IpU-0kzN2j|zH{IX5YpPTj#=hFe+Ub_2%DZS)9E5_w+GHP;YKdmp<%C;1e?yEM~ meY~rv`ZtNVugct{lwLZ7KKkvfQ|F6+*4_Eu-MJ)+H10nqPQT~? literal 0 HcmV?d00001 diff --git a/teams_app/migrations/__pycache__/0006_add_address_status_and_selected_location.cpython-313.pyc b/teams_app/migrations/__pycache__/0006_add_address_status_and_selected_location.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..769f700eed634509f4f0b3f1345c3cb5ddffa8b3 GIT binary patch literal 1809 zcmbVMOK;p%6uy4U!}Dk|&ZC*66=8^qyom!MNRc2CqEsT3O5~=yNLQZm&5Vi1HrICA zuxf*ZkXTR&vF#42>?T!{yp+jI zYRg*gfGe7ql)6@%8G+?GevT7MO%ZU10KRZn<(c zAd{gL3CpX6<>8BLI>Nax1f0hOTwIW_vLIcOrSz&SxQr|CR9~Cn3S>|(>I0(7cxXX< zO$WmeKl;DpYgoNxf)PBbi-1EJk1fcT_EE#*m*hEvgq~+*e3ez&6Iwk{J|FD&fO7&i zt_0NR`o!?OWM*z|Zk{mObvG#CpNYrl8f^t+&35k*7COW-lQOp$rpc%e9PtAq2z^b4 zOa+fRrscH5Hz9NMIQli(iJnEh=y|*~9qmTj@dHq{quqEb`YqbwWb>HYf-F4dS{5^K z>cv~}_tDcVI(``c0PZ%rKoeOVOl*?dt7(6v3zHgcNYC+0#(io7vfypIt@OZ= zVAG=<#uIF_K_5lW0JrDyL$FAluo?2TML%;SH90F%bmzzz>S4*9iMSaHajimUIY2M-hF`28% z#~p*EdQ!b<1a!sf(o9uW==~rQORh%;cgq+doH5)2 z^K};bfOOMXx9Em`&t>brXSAs9dV#xP8G6%btwU?Rp0YT5b1v(r_m~c>exCrS{Nbcc zD~CXN$0RRpVbq-y5ml zjXm|ola(DgRzJMZ>uk0j+eSIeG*OFf;RhO^&=(6J5`{A z(K8*Ki~3KecLiO|f3w792Uy+FcBcEux})lqgGv)?ENqLX{8_l}hQ5gDui=v9GBc$2NC%p&Jqs zGfV#iY>cq+7uu!OtxVa10%GMor%^g!>0Uq2y&rzx=g#AW1rNb9__`Z?#|Zt@Wct{b zV7vw31QA5=bF_m6-Z6x+ik49W5#tsjW(T*OY0~Wdz?@)V_N^*gEKFkfSXdpqUA>wx!O)6Htj=7!_Ftb(9AcBX zO_NkMO{n4$S9m?IHLu(k(|(3}2I$buIU7jIvP`XQY-~KFEDZG$TuLf)mKWemLMnO8 zgPeynPJ?nFHLo)-BULp>izFAvG=L#r4h>!0RCYIJNgq^T?`X`EUVfmgq=;j1d8HaO zVFM0(+S>aOkHar$5BjXo(nG28D22!(=76h|zA0F&qk&zcbUzX@kE4Vqsdk4v<4MSq zAmUQ!^H7y1Vfaj!6_%#%(*q_-uDmqk)04SD%SoevvOE`2x5&AqR4hWu7#328{wv9a z^hF_Ipf2l2{K$WsioVQP!2LAK)1!#_T^97A0qK{TPOMhepWOl&! zg+7vb7mI9TsOg^cnoy6V+q$-amvTuo1pzyOG fc^k~g#2Ei|5N`c;s@VDDo!6SDwdNlLq|E*Y=;G=! literal 0 HcmV?d00001 diff --git a/teams_app/migrations/__pycache__/0008_remove_graph_node_id_and_change_default_status.cpython-313.pyc b/teams_app/migrations/__pycache__/0008_remove_graph_node_id_and_change_default_status.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8dc1efd3467586e6a28eba270bed48fb24700951 GIT binary patch literal 1133 zcmY*XL2ukd6dv2V-nDl(q?;lVAXNw{k}q{GsEq`b5E6#~k%+}@rP0Km?7H#V=8YYi zTbd$-!~rDKYtL{?DnJ#7{02LhM04eo+lGJ;H{N(-EsQj8zW3g|dGCGmu6sS3;QHss zt?+$=kl&S3ujUQ7eu&^1@rb8=Og1&pHg(Wf$Rb%Kp8h)VjDfao)kX zUc+l{=;)uD*=tn!%|Uy+Ut4D9DshR|`b_iMp5@sahS%9JTpet$3!P!lnUmJ1bEPG5N9uKpTV}J^V9tzpq4S5v&M7Er+Y}2Y& zN~KLH{(c^D#9d0i$XTTNN9`3#cS8_a6vjMGlsn*x2p;<(7oes}cDDIA+2?oSN;wt4u;#3t6ldHc>;`_9E{YZtdxu9}8rPui7r z`K-NMS(h&Qi|75-v;OKIdUL5i>5`?p7jGr%NGo`R zRVDX=or>d%%4tE0q3PW*y?*>>11blW=9F5rn^ zVmxx;%H10nF@!|U9=lv1y?Wxdh{mgbS~hTSlKJ!h{Qqa(|L>pm_v;AO$`8wZAS3is z7`;_ISI(wE*+dqyq!(yLV$zJv7!#%H@Py2PleFsFmgi z{^v81ReUX3s#UVeQ;MZcDe#hJ=}d3*8I@Ffww2;Z- zf=fN;GnzB{(lX9G7^gbM5E*+E^a{rBV&aKtu&!df;4&V0u220yxE(6Wq`vJ^&cqm` z`cp5W?74WD6~u-1tVwb^)q{}sM&dfoTIkxCL=kiAaYQ-B>;|Nq!A9w#`G!-*O%`*H z_;s@6(zdxAuq7T6o0>rw1#Op@bz(081a4+T=2FmUUW}g!#7z&>sz{rax{5rcuD$H)NE&6ALfigsyT}=OTmOw&w7^iDaRnn&tt{dq~#|$ n6VvP-m=bJ$2SitqBbOg=c&foEV8G}F zoFIYj!EJZ|*t!GQ9-@J>Dp)@s+Z`6x#EIMsLn5TYoKFO;d0EKuS_|;6(Xq~+!Vm<) zAS{f8jjb37XXp;L{x9gT9>K;Ac8`o^1AD@sY(%~aUah)Zcv=`)DySLFC81TN_L3wy zA~dU+ASO%GR4^%ttZ6-y$}u_Rbxc==d6}||6f|ebG4UJpqd@tjO%b9(LbPO-F^#tf zc{ih(k!f~?kT+b&I^zW^O7l8p6)RF!j5(9S7^F4_EHCHmIcHg_!m?sZchpVNPQ=HA z*0tcHS*4$@k(^y0AkD9pvQ!5AqY>hJ{`4}vF+ZQbxby=+1VuAlk)KmGml zoxSs`yT4sK2pikI=6ya9w^r5t$W}r6mKKxpFden4cb_)e;r@>uL@s((k2 fcXeoNgwUl2=>8vX6M0|5v+aW)+Xt7RNn8CFqk+2! literal 0 HcmV?d00001 diff --git a/teams_app/migrations/__pycache__/0011_teaminvitationtoken.cpython-313.pyc b/teams_app/migrations/__pycache__/0011_teaminvitationtoken.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4f2c106ea28632abcac962e436d44e154c357bd GIT binary patch literal 1930 zcmZ`)O-vg{6rSC+*FS)l=LMYy44z?wcMZwPi zqDxfK4#XSHNuZGe zjWeftDa6($PRiF}r^qHxkxiYHX=B>Bc1Gl$4QXe!b90h*KBquchO`MfTux^ud1SR$ z-~BK+fQm<4Y*ZcHsMUCCZtmioPI1+)V_k@K$2DBf;lp`gu3L2iEVM@6-ozG{iJ4#K z^7U((0PiUcpw`-QcQck-Hw|QC9@iHw5#wv+3q~#lF`~ zHlx6yLN#oe#439iK!rVJKeAo+1KVcL8jmK~3%1jE2+R(9(Rjq3vt2Myjgm|1*c0fp z$Lu+5?6U9KGxiH?1~H{J^~5b=Pa9vu&JLJu({T{&i%i4;N*gvQVTTR_@2AJ3_eUIe~G*1#9(B8h1x2L zTx=_HQhj){9YvAhR(PTrp4dyg{byq2alMtAX{Kg&1Fxpp9i3UUk#bnv-AkmNjJC!z z&GF3k^iMN8Gp~l(?N1xycUb(hy~Oz^^;SCDOlNm5y>uJtn=HP%mzer?xivlCoSy$1 zp{qo0Ljv*_V!5uYD6bKE>!t!sVpZwdUuvc={g%GW;#ZCdUM3sqWfuRqE1GMj=XS6B zn)^A|T3Bf;th5&LjfH$`q0m?;uuTX2ZAqF8&L{ICQsTDU9t(Q9mg$IXg8krC7ftH8B@T#wQjO=;vmp z7bTWt=I0gb$H!;pWtPOp>lIYq;;_lhPbtkwwJTx;+5&P#F^KVznURsPh#ANN0A2ns A*Z=?k literal 0 HcmV?d00001 diff --git a/teams_app/models.py b/teams_app/models.py new file mode 100644 index 0000000..32bab42 --- /dev/null +++ b/teams_app/models.py @@ -0,0 +1,165 @@ +from django.conf import settings +from django.db import models +import uuid + +class Team(models.Model): + TEAM_TYPE_CHOICES = [ + ('BUYER', 'Покупатель'), + ('SELLER', 'Продавец'), + ] + + LOCATION_TYPE_CHOICES = [ + ('address', 'Адрес'), + ('hub', 'Хаб'), + ] + + uuid = models.CharField(max_length=100, unique=True, default=uuid.uuid4) + name = models.CharField(max_length=255) + team_type = models.CharField(max_length=20, choices=TEAM_TYPE_CHOICES, default='BUYER') + logto_org_id = models.CharField(max_length=255, null=True, blank=True) + owner = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='owned_teams', + null=True, + blank=True, + ) + selected_location_type = models.CharField(max_length=20, choices=LOCATION_TYPE_CHOICES, null=True, blank=True) + selected_location_uuid = models.CharField(max_length=100, null=True, blank=True) + selected_location_name = models.CharField(max_length=255, null=True, blank=True) + selected_location_latitude = models.FloatField(null=True, blank=True) + selected_location_longitude = models.FloatField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'teams_team' + + def __str__(self): + return f"Team {self.name}" + + +class UserProfile(models.Model): + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='profile' + ) + logto_id = models.CharField(max_length=255, unique=True) + avatar_id = models.CharField(max_length=100, blank=True, null=True) + phone = models.CharField(max_length=20, blank=True, null=True) + active_team = models.ForeignKey(Team, on_delete=models.SET_NULL, null=True, blank=True, related_name='active_profiles') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'teams_user_profile' + + def __str__(self): + return f"Profile {self.logto_id}" + +class TeamMember(models.Model): + ROLE_CHOICES = [ + ('OWNER', 'Владелец'), + ('ADMIN', 'Администратор'), + ('MANAGER', 'Менеджер'), + ('MEMBER', 'Участник'), + ] + + uuid = models.CharField(max_length=100, unique=True, default=uuid.uuid4) + team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name='members') + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='team_memberships', + null=True, + blank=True, + ) + role = models.CharField(max_length=50, choices=ROLE_CHOICES, default='MEMBER') + joined_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = 'teams_member' + unique_together = ['team', 'user'] + + def __str__(self): + return f"{self.team.name} - {self.user} ({self.role})" + +class TeamInvitation(models.Model): + INVITATION_STATUS_CHOICES = [ + ('PENDING', 'Ожидает ответа'), + ('ACCEPTED', 'Принято'), + ('DECLINED', 'Отклонено'), + ('EXPIRED', 'Истекло'), + ] + + ROLE_CHOICES = TeamMember.ROLE_CHOICES + + uuid = models.CharField(max_length=100, unique=True, default=uuid.uuid4) + team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name='invitations') + email = models.EmailField() + role = models.CharField(max_length=50, choices=ROLE_CHOICES, default='MEMBER') + status = models.CharField(max_length=50, choices=INVITATION_STATUS_CHOICES, default='PENDING') + invited_by = models.CharField(max_length=255) + expires_at = models.DateTimeField() + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = 'teams_invitation' + unique_together = ['team', 'email'] + + def __str__(self): + return f"Приглашение в {self.team.name} для {self.email}" + + +class TeamInvitationToken(models.Model): + WORKFLOW_STATUS_CHOICES = [ + ('pending', 'Ожидает обработки'), + ('active', 'Активен'), + ('error', 'Ошибка'), + ] + + uuid = models.CharField(max_length=100, unique=True, default=uuid.uuid4) + invitation = models.ForeignKey(TeamInvitation, on_delete=models.CASCADE, related_name='tokens') + token_hash = models.CharField(max_length=255, unique=True) + workflow_status = models.CharField( + max_length=20, + choices=WORKFLOW_STATUS_CHOICES, + default='pending', + ) + expires_at = models.DateTimeField() + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = 'teams_invitation_token' + + def __str__(self): + return f"Token {self.uuid} for invitation {self.invitation_id}" + + +class TeamAddress(models.Model): + ADDRESS_STATUS_CHOICES = [ + ('pending', 'Ожидает обработки'), + ('active', 'Активен'), + ('error', 'Ошибка'), + ] + + uuid = models.CharField(max_length=100, unique=True, default=uuid.uuid4) + team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name='addresses') + name = models.CharField(max_length=255) # "Офис", "Склад", "Производство" + address = models.TextField() + latitude = models.FloatField(null=True, blank=True) + longitude = models.FloatField(null=True, blank=True) + country_code = models.CharField(max_length=2, null=True, blank=True) # ISO 3166-1 alpha-2 + is_default = models.BooleanField(default=False) + status = models.CharField(max_length=20, choices=ADDRESS_STATUS_CHOICES, default='pending') + processed_at = models.DateTimeField(null=True, blank=True) + error_message = models.TextField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'teams_address' + + def __str__(self): + return f"{self.team.name} - {self.name}" diff --git a/teams_app/permissions.py b/teams_app/permissions.py new file mode 100644 index 0000000..d44f02d --- /dev/null +++ b/teams_app/permissions.py @@ -0,0 +1,74 @@ +""" +Декоратор для проверки scopes в JWT токене. +Используется для защиты GraphQL резолверов. +""" +from functools import wraps +from graphql import GraphQLError + + +def require_scopes(*scopes: str): + """ + Декоратор для проверки наличия scopes в JWT токене. + + Использование: + @require_scopes("read:teams") + def resolve_team(self, info): + ... + + @require_scopes("read:teams", "write:teams") + def resolve_update_team(self, info): + ... + """ + def decorator(func): + # Сохраняем scopes в метаданных для возможности сбора всех scopes + if not hasattr(func, '_required_scopes'): + func._required_scopes = [] + func._required_scopes.extend(scopes) + + @wraps(func) + def wrapper(self, info, *args, **kwargs): + # Получаем scopes из контекста (должны быть добавлены в middleware) + user_scopes = set(getattr(info.context, 'scopes', []) or []) + + missing = set(scopes) - user_scopes + if missing: + raise GraphQLError(f"Missing required scopes: {', '.join(missing)}") + + return func(self, info, *args, **kwargs) + + # Переносим метаданные на wrapper + wrapper._required_scopes = func._required_scopes + return wrapper + return decorator + + +def collect_scopes_from_schema(schema) -> set: + """ + Собирает все scopes из схемы для синхронизации с Logto. + + Использование: + from .schema import schema + scopes = collect_scopes_from_schema(schema) + # {'read:team', 'invite:member', ...} + """ + scopes = set() + + # Query resolvers + if hasattr(schema, 'query') and schema.query: + query_type = schema.query + for field_name in dir(query_type): + if field_name.startswith('resolve_'): + resolver = getattr(query_type, field_name, None) + if resolver and hasattr(resolver, '_required_scopes'): + scopes.update(resolver._required_scopes) + + # Mutation resolvers + if hasattr(schema, 'mutation') and schema.mutation: + mutation_type = schema.mutation + for field_name, field in mutation_type._meta.fields.items(): + if hasattr(field, 'type') and hasattr(field.type, 'mutate'): + mutate = field.type.mutate + if hasattr(mutate, '_required_scopes'): + scopes.update(mutate._required_scopes) + + return scopes diff --git a/teams_app/schemas/__pycache__/m2m_schema.cpython-313.pyc b/teams_app/schemas/__pycache__/m2m_schema.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e7a60ffaa98918a7bb8c2b28b5aff7a46f36b04 GIT binary patch literal 18693 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 b2JmCvK_I>!zZ$=F?5- Tuple[str, str]: + """ + Start the team_created workflow in Temporal and return (workflow_id, run_id). + """ + client = await Client.connect(TEMPORAL_ADDRESS, namespace=TEMPORAL_NAMESPACE) + + # We re-use team.uuid as workflow_id to keep idempotency. + handle = await client.start_workflow( + "team_created_workflow", # workflow name registered in worker + { + "team_id": team.uuid, + "team_name": team.name, + "owner_id": getattr(getattr(team.owner, "profile", None), "logto_id", "") or getattr(team.owner, "username", ""), + "logto_org_id": team.logto_org_id or "", + }, + id=team.uuid, + task_queue=TEMPORAL_TASK_QUEUE, + ) + return handle.id, handle.run_id + + +def start_team_created(team: Team) -> Tuple[str, str]: + """ + Sync wrapper for Django mutation handlers. + """ + try: + return asyncio.run(_start_team_created_async(team)) + except Exception: + logger.exception("Failed to start Temporal workflow for team %s", team.uuid) + raise + + +async def _start_address_workflow_async( + team_uuid: str, + name: str, + address: str, + latitude: float | None = None, + longitude: float | None = None, + country_code: str | None = None, + is_default: bool = False, +) -> Tuple[str, str]: + """ + Start the create_address workflow in Temporal. + Returns (workflow_id, run_id). + """ + client = await Client.connect(TEMPORAL_ADDRESS, namespace=TEMPORAL_NAMESPACE) + + workflow_id = f"address-{uuid.uuid4()}" + + handle = await client.start_workflow( + "create_address", + { + "workflow_id": workflow_id, + "team_uuid": team_uuid, + "name": name, + "address": address, + "latitude": latitude, + "longitude": longitude, + "country_code": country_code, + "is_default": is_default, + }, + id=workflow_id, + task_queue=TEMPORAL_TASK_QUEUE, + ) + + logger.info("Started address workflow %s for team %s", workflow_id, team_uuid) + return handle.id, handle.result_run_id + + +def start_address_workflow( + team_uuid: str, + name: str, + address: str, + latitude: float | None = None, + longitude: float | None = None, + country_code: str | None = None, + is_default: bool = False, +) -> Tuple[str, str]: + """ + Sync wrapper for starting address workflow. + """ + try: + return asyncio.run(_start_address_workflow_async( + team_uuid=team_uuid, + name=name, + address=address, + latitude=latitude, + longitude=longitude, + country_code=country_code, + is_default=is_default, + )) + except Exception: + logger.exception("Failed to start address workflow for team %s", team_uuid) + raise + + +async def _start_invite_workflow_async( + team_uuid: str, + email: str, + role: str, + invited_by: str, + expires_at: str, +) -> Tuple[str, str]: + """ + Start the invite_user workflow in Temporal. + Returns (workflow_id, run_id). + """ + client = await Client.connect(TEMPORAL_ADDRESS, namespace=TEMPORAL_NAMESPACE) + + workflow_id = f"invite-{uuid.uuid4()}" + + handle = await client.start_workflow( + "invite_user", + { + "team_uuid": team_uuid, + "email": email, + "role": role, + "invited_by": invited_by, + "expires_at": expires_at, + }, + id=workflow_id, + task_queue=TEMPORAL_TASK_QUEUE, + ) + + logger.info("Started invite workflow %s for %s", workflow_id, email) + return handle.id, handle.result_run_id + + +def start_invite_workflow( + team_uuid: str, + email: str, + role: str, + invited_by: str, + expires_at: str, +) -> Tuple[str, str]: + """ + Sync wrapper for starting invite workflow. + """ + try: + return asyncio.run(_start_invite_workflow_async( + team_uuid=team_uuid, + email=email, + role=role, + invited_by=invited_by, + expires_at=expires_at, + )) + except Exception: + logger.exception("Failed to start invite workflow for %s", email) + raise diff --git a/teams_app/tests.py b/teams_app/tests.py new file mode 100644 index 0000000..acf7551 --- /dev/null +++ b/teams_app/tests.py @@ -0,0 +1,98 @@ +from django.test import TestCase +from graphene.test import Client +from teams_app.schema import schema + +class TeamsGraphQLTestCase(TestCase): + def setUp(self): + self.client = Client(schema) + + def test_get_user_teams_with_params(self): + """Тест getUserTeams с userId""" + query = ''' + { + getUserTeams(userId: "demo-user") { + id + name + ownerId + logtoOrgId + createdAt + updatedAt + } + } + ''' + result = self.client.execute(query) + print(f"\n=== getUserTeams WITH PARAMS ===") + print(f"Result: {result}") + + if result.get('errors'): + print(f"ERRORS: {result['errors']}") + + if result.get('data'): + teams = result['data']['getUserTeams'] + print(f"Found {len(teams)} teams") + for team in teams: + print(f"Team: {team.get('name')} - {team.get('id')}") + + # Проверки + self.assertIsNone(result.get('errors')) + self.assertIn('getUserTeams', result['data']) + + def test_get_user_teams_no_params(self): + """Тест getUserTeams без параметров""" + query = ''' + { + getUserTeams { + id + name + } + } + ''' + result = self.client.execute(query) + print(f"\n=== getUserTeams NO PARAMS ===") + print(f"Result: {result}") + + if result.get('errors'): + print(f"ERRORS: {result['errors']}") + + if result.get('data'): + teams = result['data']['getUserTeams'] + print(f"Found {len(teams)} teams") + + def test_schema_fields(self): + """Тест что схема содержит нужные поля""" + query = ''' + { + __type(name: "Team") { + fields { + name + type { + name + } + } + } + } + ''' + result = self.client.execute(query) + print(f"\n=== TEAM SCHEMA FIELDS ===") + + if result.get('data') and result['data']['__type']: + fields = result['data']['__type']['fields'] + field_names = [f['name'] for f in fields] + print(f"Team fields: {field_names}") + + required_fields = ['id', 'name', 'ownerId'] + for field in required_fields: + if field in field_names: + print(f"✅ {field} - OK") + else: + print(f"❌ {field} - MISSING") + + def test_invalid_query(self): + """Тест неправильного запроса""" + query = '{ nonExistentField }' + result = self.client.execute(query) + print(f"\n=== INVALID QUERY TEST ===") + print(f"Result: {result}") + + # Должна быть ошибка + self.assertIsNotNone(result.get('errors')) diff --git a/teams_app/views.py b/teams_app/views.py new file mode 100644 index 0000000..3effa15 --- /dev/null +++ b/teams_app/views.py @@ -0,0 +1,97 @@ +import json +import jwt +from django.conf import settings +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +from jwt import InvalidTokenError + +from .auth import get_bearer_token, scopes_from_payload, validator + + +@csrf_exempt +def test_jwt(request): + """Тестовый endpoint для проверки JWT токена с подписью.""" + + try: + token = get_bearer_token(request) + except InvalidTokenError as exc: + return JsonResponse({"status": "error", "error": str(exc)}, status=403) + + response = {"token_length": len(token), "token_preview": f"{token[:32]}...{token[-32:]}"} + + try: + audience = getattr(settings, "LOGTO_TEAMS_AUDIENCE", None) + payload = validator.decode(token, audience=audience) + response.update( + { + "status": "ok", + "header": jwt.get_unverified_header(token), + "payload": payload, + "user_id": payload.get("sub"), + "team_uuid": payload.get("team_uuid"), + "scopes": scopes_from_payload(payload), + } + ) + return JsonResponse(response, json_dumps_params={"indent": 2}) + except InvalidTokenError as exc: + response["status"] = "invalid" + response["error"] = str(exc) + return JsonResponse(response, status=403, json_dumps_params={"indent": 2}) + + +# GraphQL Views - authentication handled by GRAPHENE MIDDLEWARE + +from graphene_django.views import GraphQLView + +from .graphql_middleware import ( + M2MNoAuthMiddleware, + PublicNoAuthMiddleware, + TeamJWTMiddleware, + UserJWTMiddleware, +) + + +def _is_introspection_query(request): + """Проверяет, является ли запрос introspection (для GraphQL codegen)""" + if request.method != 'POST': + return False + try: + body = json.loads(request.body.decode('utf-8')) + query = body.get('query', '') + return '__schema' in query or '__type' in query + except Exception: + return False + + +class PublicGraphQLView(GraphQLView): + """GraphQL view for public operations (no authentication).""" + + def __init__(self, *args, **kwargs): + kwargs['middleware'] = [PublicNoAuthMiddleware()] + super().__init__(*args, **kwargs) + + +class UserGraphQLView(GraphQLView): + """GraphQL view for user-level operations (ID Token).""" + + def __init__(self, *args, **kwargs): + kwargs['middleware'] = [UserJWTMiddleware()] + super().__init__(*args, **kwargs) + + +class TeamGraphQLView(GraphQLView): + """GraphQL view for team-level operations (Access Token).""" + + def __init__(self, *args, **kwargs): + kwargs['middleware'] = [TeamJWTMiddleware()] + super().__init__(*args, **kwargs) + + +class M2MGraphQLView(GraphQLView): + """GraphQL view for M2M (machine-to-machine) operations. + No authentication required - used by internal services (Temporal, etc.) + """ + + def __init__(self, *args, **kwargs): + kwargs['middleware'] = [M2MNoAuthMiddleware()] + super().__init__(*args, **kwargs)