diff --git a/backend/bin/test_redis.sh b/backend/bin/test_redis.sh deleted file mode 100755 index ccfa5bd05..000000000 --- a/backend/bin/test_redis.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2023 JWP Consulting GK -set -e -poetry run pytest \ - -x \ - --pdb \ - --ds=projectify.settings.test \ - --dc=TestRedis \ - "workspace/test/test_consumers.py" diff --git a/backend/mypy/stubs/channels/__init__.pyi b/backend/mypy/stubs/channels/__init__.pyi deleted file mode 100644 index 0eb6cdb13..000000000 --- a/backend/mypy/stubs/channels/__init__.pyi +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-FileCopyrightText: 2024 JWP Consulting GK -# -# SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/backend/mypy/stubs/channels/auth.pyi b/backend/mypy/stubs/channels/auth.pyi deleted file mode 100644 index 7b2a6f110..000000000 --- a/backend/mypy/stubs/channels/auth.pyi +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-FileCopyrightText: 2024 JWP Consulting GK -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -from django.core.handlers.asgi import ASGIHandler - -from channels.middleware import BaseMiddleware - -class AuthMiddlewareStack(BaseMiddleware): - def __init__(self, inner: ASGIHandler) -> None: ... diff --git a/backend/mypy/stubs/channels/consumer.pyi b/backend/mypy/stubs/channels/consumer.pyi deleted file mode 100644 index d54de0c90..000000000 --- a/backend/mypy/stubs/channels/consumer.pyi +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-FileCopyrightText: 2024 JWP Consulting GK -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -from typing import Any - -from django.core.handlers.asgi import ASGIHandler - -from .layers import BaseChannelLayer - -class AsyncConsumer: - scope: dict[str, Any] - channel_layer: BaseChannelLayer - channel_name: str - - @classmethod - def as_asgi(cls, **initkwargs: Any) -> ASGIHandler: ... - -class SyncConsumer(AsyncConsumer): ... diff --git a/backend/mypy/stubs/channels/db.pyi b/backend/mypy/stubs/channels/db.pyi deleted file mode 100644 index a4504e4f5..000000000 --- a/backend/mypy/stubs/channels/db.pyi +++ /dev/null @@ -1,14 +0,0 @@ -# SPDX-FileCopyrightText: 2024 JWP Consulting GK -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -from collections.abc import Awaitable -from typing import Callable, ParamSpec, TypeVar - -T = TypeVar("T") -R = TypeVar("R") -P = ParamSpec("P") - -def database_sync_to_async( - fn: Callable[P, R], -) -> Callable[P, Awaitable[R]]: ... diff --git a/backend/mypy/stubs/channels/generic/__init__.pyi b/backend/mypy/stubs/channels/generic/__init__.pyi deleted file mode 100644 index 0eb6cdb13..000000000 --- a/backend/mypy/stubs/channels/generic/__init__.pyi +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-FileCopyrightText: 2024 JWP Consulting GK -# -# SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/backend/mypy/stubs/channels/generic/websocket.pyi b/backend/mypy/stubs/channels/generic/websocket.pyi deleted file mode 100644 index e25b0577d..000000000 --- a/backend/mypy/stubs/channels/generic/websocket.pyi +++ /dev/null @@ -1,17 +0,0 @@ -# SPDX-FileCopyrightText: 2024 JWP Consulting GK -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -from typing import Optional - -from ..consumer import SyncConsumer - -class WebsocketConsumer(SyncConsumer): - def connect(self) -> None: ... - def accept(self, subprotocol: Optional[str] = None) -> None: ... - def close(self, code: Optional[int] = None) -> None: ... - -class JsonWebsocketConsumer(WebsocketConsumer): - def send_json( - self, content: object, close: Optional[bool] = False - ) -> None: ... diff --git a/backend/mypy/stubs/channels/layers.pyi b/backend/mypy/stubs/channels/layers.pyi deleted file mode 100644 index 75f1892e5..000000000 --- a/backend/mypy/stubs/channels/layers.pyi +++ /dev/null @@ -1,17 +0,0 @@ -# SPDX-FileCopyrightText: 2024 JWP Consulting GK -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -from abc import ABCMeta, abstractmethod -from typing import Optional - -class BaseChannelLayer(metaclass=ABCMeta): - async def group_send( - self, group: str, message: dict[str, object] - ) -> None: ... - @abstractmethod - def group_add(self, group: str, channel: str) -> None: ... - @abstractmethod - def group_discard(self, group: str, channel: str) -> None: ... - -def get_channel_layer() -> Optional[BaseChannelLayer]: ... diff --git a/backend/mypy/stubs/channels/middleware.pyi b/backend/mypy/stubs/channels/middleware.pyi deleted file mode 100644 index b80c312f8..000000000 --- a/backend/mypy/stubs/channels/middleware.pyi +++ /dev/null @@ -1,8 +0,0 @@ -# SPDX-FileCopyrightText: 2024 JWP Consulting GK -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -from django.core.handlers.asgi import ASGIHandler - -class BaseMiddleware(ASGIHandler): - def __init__(self, inner: BaseMiddleware) -> None: ... diff --git a/backend/mypy/stubs/channels/routing.pyi b/backend/mypy/stubs/channels/routing.pyi deleted file mode 100644 index 278d65e49..000000000 --- a/backend/mypy/stubs/channels/routing.pyi +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-FileCopyrightText: 2024 JWP Consulting GK -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -from collections.abc import Sequence -from typing import TypedDict - -from django.core.handlers.asgi import ASGIHandler -from django.urls import URLPattern - -class ProtocolRoutes(TypedDict): - http: ASGIHandler - websocket: ASGIHandler - -class URLRouter(ASGIHandler): - def __init__(self, url: Sequence[URLPattern]) -> None: ... - -class ProtocolTypeRouter(ASGIHandler): - def __init__(self, routes: ProtocolRoutes) -> None: ... diff --git a/backend/mypy/stubs/channels/security/websocket.pyi b/backend/mypy/stubs/channels/security/websocket.pyi deleted file mode 100644 index e0688c1e8..000000000 --- a/backend/mypy/stubs/channels/security/websocket.pyi +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-FileCopyrightText: 2024 JWP Consulting GK -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -"""Bindings for channels.security.websocket.""" - -from collections.abc import Sequence - -from django.core.handlers.asgi import ASGIHandler - -from channels.middleware import BaseMiddleware - -class OriginValidator(BaseMiddleware): - def __init__( - self, inner: ASGIHandler, allowed_origins: Sequence[str] - ) -> None: ... - -class AllowedHostsOriginValidator(OriginValidator): - def __init__(self, inner: ASGIHandler) -> None: ... diff --git a/backend/mypy/stubs/channels/testing.pyi b/backend/mypy/stubs/channels/testing.pyi deleted file mode 100644 index 240100045..000000000 --- a/backend/mypy/stubs/channels/testing.pyi +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-FileCopyrightText: 2024 JWP Consulting GK -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -from typing import Any, Union - -JsonData = Union[dict[str, JsonData], list[JsonData], str, int, bool] - -class WebsocketCommunicator: - scope: dict[str, Any] - - def __init__(self, asgi_application: object, resource: str) -> None: ... - - # connected, subprotocol - async def connect(self) -> tuple[bool, object]: ... - async def disconnect(self) -> None: ... - async def send_json_to(self, data: JsonData) -> None: ... - async def receive_json_from(self) -> JsonData: ... - async def receive_nothing(self) -> bool: ... - - # From agiref.testing.ApplicationCommuniactor.receive_output - async def receive_output(self, timeout: int = 1) -> dict[str, Any]: ... diff --git a/backend/poetry.lock b/backend/poetry.lock index a3abe2c45..d0c06231d 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -72,54 +72,6 @@ files = [ {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, ] -[[package]] -name = "autobahn" -version = "23.6.2" -description = "WebSocket client & server library, WAMP real-time framework" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "autobahn-23.6.2.tar.gz", hash = "sha256:ec9421c52a2103364d1ef0468036e6019ee84f71721e86b36fe19ad6966c1181"}, -] - -[package.dependencies] -cryptography = ">=3.4.6" -hyperlink = ">=21.0.0" -setuptools = "*" -txaio = ">=21.2.1" - -[package.extras] -all = ["PyGObject (>=3.40.0)", "argon2_cffi (>=20.1.0)", "attrs (>=20.3.0)", "base58 (>=2.1.0)", "bitarray (>=2.7.5)", "cbor2 (>=5.2.0)", "cffi (>=1.14.5)", "click (>=8.1.2)", "ecdsa (>=0.16.1)", "eth-abi (>=4.0.0)", "flatbuffers (>=22.12.6)", "hkdf (>=0.0.3)", "jinja2 (>=2.11.3)", "mnemonic (>=0.19)", "msgpack (>=1.0.2)", "passlib (>=1.7.4)", "py-ecc (>=5.1.0)", "py-eth-sig-utils (>=0.4.0)", "py-multihash (>=2.0.1)", "py-ubjson (>=0.16.1)", "pynacl (>=1.4.0)", "pyopenssl (>=20.0.1)", "python-snappy (>=0.6.0)", "pytrie (>=0.4.0)", "qrcode (>=7.3.1)", "rlp (>=2.0.1)", "service_identity (>=18.1.0)", "spake2 (>=0.8)", "twisted (>=20.3.0)", "ujson (>=4.0.2)", "web3[ipfs] (>=6.0.0)", "xbr (>=21.2.1)", "yapf (==0.29.0)", "zlmdb (>=21.2.1)", "zope.interface (>=5.2.0)"] -compress = ["python-snappy (>=0.6.0)"] -dev = ["backports.tempfile (>=1.0)", "bumpversion (>=0.5.3)", "codecov (>=2.0.15)", "flake8 (<5)", "humanize (>=0.5.1)", "mypy (>=0.610) ; python_version >= \"3.4\" and platform_python_implementation != \"PyPy\"", "passlib", "pep8-naming (>=0.3.3)", "pip (>=9.0.1)", "pyenchant (>=1.6.6)", "pyflakes (>=1.0.0)", "pyinstaller (>=4.2)", "pylint (>=1.9.2)", "pytest (>=3.4.2)", "pytest-aiohttp", "pytest-asyncio (>=0.14.0)", "pytest-runner (>=2.11.1)", "pyyaml (>=4.2b4)", "qualname", "sphinx (>=1.7.1)", "sphinx-autoapi (>=1.7.0)", "sphinx_rtd_theme (>=0.1.9)", "sphinxcontrib-images (>=0.9.1)", "tox (>=4.2.8)", "tox-gh-actions (>=2.2.0)", "twine (>=3.3.0)", "twisted (>=22.10.0)", "txaio (>=20.4.1)", "watchdog (>=0.8.3)", "wheel (>=0.36.2)", "yapf (==0.29.0)"] -encryption = ["pynacl (>=1.4.0)", "pyopenssl (>=20.0.1)", "pytrie (>=0.4.0)", "qrcode (>=7.3.1)", "service_identity (>=18.1.0)"] -nvx = ["cffi (>=1.14.5)"] -scram = ["argon2_cffi (>=20.1.0)", "cffi (>=1.14.5)", "passlib (>=1.7.4)"] -serialization = ["cbor2 (>=5.2.0)", "flatbuffers (>=22.12.6)", "msgpack (>=1.0.2)", "py-ubjson (>=0.16.1)", "ujson (>=4.0.2)"] -twisted = ["attrs (>=20.3.0)", "twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] -ui = ["PyGObject (>=3.40.0)"] -xbr = ["base58 (>=2.1.0)", "bitarray (>=2.7.5)", "cbor2 (>=5.2.0)", "click (>=8.1.2)", "ecdsa (>=0.16.1)", "eth-abi (>=4.0.0)", "hkdf (>=0.0.3)", "jinja2 (>=2.11.3)", "mnemonic (>=0.19)", "py-ecc (>=5.1.0)", "py-eth-sig-utils (>=0.4.0)", "py-multihash (>=2.0.1)", "rlp (>=2.0.1)", "spake2 (>=0.8)", "twisted (>=20.3.0)", "web3[ipfs] (>=6.0.0)", "xbr (>=21.2.1)", "yapf (==0.29.0)", "zlmdb (>=21.2.1)"] - -[[package]] -name = "automat" -version = "22.10.0" -description = "Self-service finite-state machines for the programmer on the go." -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "Automat-22.10.0-py2.py3-none-any.whl", hash = "sha256:c3164f8742b9dc440f3682482d32aaff7bb53f71740dd018533f9de286b64180"}, - {file = "Automat-22.10.0.tar.gz", hash = "sha256:e56beb84edad19dcc11d30e8d9b895f75deeb5ef5e96b84a467066b3b84bb04e"}, -] - -[package.dependencies] -attrs = ">=19.2.0" -six = "*" - -[package.extras] -visualize = ["Twisted (>=16.1.1)", "graphviz (>0.5.1)"] - [[package]] name = "billiard" version = "4.2.1" @@ -312,54 +264,11 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] -markers = {main = "platform_python_implementation != \"PyPy\"", dev = "os_name == \"nt\" and implementation_name != \"pypy\" or platform_python_implementation != \"PyPy\"", test = "platform_python_implementation != \"PyPy\""} +markers = {main = "platform_python_implementation != \"PyPy\"", dev = "os_name == \"nt\" and implementation_name != \"pypy\"", test = "platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = "*" -[[package]] -name = "channels" -version = "4.1.0" -description = "Brings async, event-driven capabilities to Django 3.2 and up." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "channels-4.1.0-py3-none-any.whl", hash = "sha256:a3c4419307f582c3f71d67bfb6eff748ae819c2f360b9b141694d84f242baa48"}, - {file = "channels-4.1.0.tar.gz", hash = "sha256:e0ed375719f5c1851861f05ed4ce78b0166f9245ca0ecd836cb77d4bb531489d"}, -] - -[package.dependencies] -asgiref = ">=3.6.0,<4" -daphne = {version = ">=4.0.0", optional = true, markers = "extra == \"daphne\""} -Django = ">=4.2" - -[package.extras] -daphne = ["daphne (>=4.0.0)"] -tests = ["async-timeout", "coverage (>=4.5,<5.0)", "pytest", "pytest-asyncio", "pytest-django"] - -[[package]] -name = "channels-redis" -version = "4.2.0" -description = "Redis-backed ASGI channel layer implementation" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "channels_redis-4.2.0-py3-none-any.whl", hash = "sha256:2c5b944a39bd984b72aa8005a3ae11637bf29b5092adeb91c9aad4ab819a8ac4"}, - {file = "channels_redis-4.2.0.tar.gz", hash = "sha256:01c26c4d5d3a203f104bba9e5585c0305a70df390d21792386586068162027fd"}, -] - -[package.dependencies] -asgiref = ">=3.2.10,<4" -channels = "*" -msgpack = ">=1.0,<2.0" -redis = ">=4.6" - -[package.extras] -cryptography = ["cryptography (>=1.3.0)"] -tests = ["async-timeout", "cryptography (>=1.3.0)", "pytest", "pytest-asyncio", "pytest-timeout"] - [[package]] name = "charset-normalizer" version = "3.2.0" @@ -544,18 +453,6 @@ files = [ ] markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} -[[package]] -name = "constantly" -version = "15.1.0" -description = "Symbolic constants in Python" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "constantly-15.1.0-py2.py3-none-any.whl", hash = "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"}, - {file = "constantly-15.1.0.tar.gz", hash = "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35"}, -] - [[package]] name = "coverage" version = "7.5.3" @@ -627,7 +524,7 @@ version = "44.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" -groups = ["main", "dev", "test"] +groups = ["main", "test"] files = [ {file = "cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009"}, {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f"}, @@ -691,26 +588,6 @@ editorconfig = ">=0.12.2" jsbeautifier = "*" six = ">=1.13.0" -[[package]] -name = "daphne" -version = "4.0.0" -description = "Django ASGI (HTTP/WebSocket) server" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "daphne-4.0.0-py3-none-any.whl", hash = "sha256:a288ece46012b6b719c37150be67c69ebfca0793a8521bf821533bad983179b2"}, - {file = "daphne-4.0.0.tar.gz", hash = "sha256:cce9afc8f49a4f15d4270b8cfb0e0fe811b770a5cc795474e97e4da287497666"}, -] - -[package.dependencies] -asgiref = ">=3.5.2,<4" -autobahn = ">=22.4.2" -twisted = {version = ">=22.4", extras = ["tls"]} - -[package.extras] -tests = ["django", "hypothesis", "pytest", "pytest-asyncio"] - [[package]] name = "decorator" version = "5.1.1" @@ -1145,21 +1022,6 @@ files = [ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] -[[package]] -name = "hyperlink" -version = "21.0.0" -description = "A featureful, immutable, and correct URL for Python." -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["dev"] -files = [ - {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"}, - {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"}, -] - -[package.dependencies] -idna = ">=2.5" - [[package]] name = "idna" version = "3.7" @@ -1196,24 +1058,6 @@ perf = ["ipython"] test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] -[[package]] -name = "incremental" -version = "24.7.2" -description = "A small library that versions your Python projects." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe"}, - {file = "incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9"}, -] - -[package.dependencies] -setuptools = ">=61.0" - -[package.extras] -scripts = ["click (>=6.0)"] - [[package]] name = "inflection" version = "0.5.1" @@ -1513,79 +1357,6 @@ files = [ [package.dependencies] traitlets = "*" -[[package]] -name = "msgpack" -version = "1.0.5" -description = "MessagePack serializer" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, - {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, - {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"}, - {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"}, - {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"}, - {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"}, - {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"}, - {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"}, - {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"}, - {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"}, - {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, - {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, - {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, - {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, - {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, - {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, -] - [[package]] name = "mypy" version = "1.19.1" @@ -1997,33 +1768,6 @@ files = [ [package.extras] tests = ["pytest"] -[[package]] -name = "pyasn1" -version = "0.5.0" -description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -groups = ["dev"] -files = [ - {file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"}, - {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, -] - -[[package]] -name = "pyasn1-modules" -version = "0.3.0" -description = "A collection of ASN.1-based protocols modules" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -groups = ["dev"] -files = [ - {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, - {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, -] - -[package.dependencies] -pyasn1 = ">=0.4.6,<0.6.0" - [[package]] name = "pycparser" version = "2.21" @@ -2035,7 +1779,7 @@ files = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] -markers = {main = "platform_python_implementation != \"PyPy\"", dev = "os_name == \"nt\" and implementation_name != \"pypy\" or platform_python_implementation != \"PyPy\"", test = "platform_python_implementation != \"PyPy\""} +markers = {main = "platform_python_implementation != \"PyPy\"", dev = "os_name == \"nt\" and implementation_name != \"pypy\"", test = "platform_python_implementation != \"PyPy\""} [[package]] name = "pygments" @@ -2052,25 +1796,6 @@ files = [ [package.extras] plugins = ["importlib-metadata ; python_version < \"3.8\""] -[[package]] -name = "pyopenssl" -version = "24.3.0" -description = "Python wrapper module around the OpenSSL library" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a"}, - {file = "pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36"}, -] - -[package.dependencies] -cryptography = ">=41.0.5,<45" - -[package.extras] -docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"] -test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] - [[package]] name = "pyright" version = "1.1.408" @@ -2643,38 +2368,13 @@ typing_extensions = ">=4.15.0,<5.0" urllib3 = {version = ">=2.6.3,<3.0", extras = ["socks"]} websocket-client = ">=1.8.0,<2.0" -[[package]] -name = "service-identity" -version = "23.1.0" -description = "Service identity verification for pyOpenSSL & cryptography." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "service_identity-23.1.0-py3-none-any.whl", hash = "sha256:87415a691d52fcad954a500cb81f424d0273f8e7e3ee7d766128f4575080f383"}, - {file = "service_identity-23.1.0.tar.gz", hash = "sha256:ecb33cd96307755041e978ab14f8b14e13b40f1fbd525a4dc78f46d2b986431d"}, -] - -[package.dependencies] -attrs = ">=19.1.0" -cryptography = "*" -pyasn1 = "*" -pyasn1-modules = "*" - -[package.extras] -dev = ["pyopenssl", "service-identity[docs,idna,mypy,tests]"] -docs = ["furo", "myst-parser", "pyopenssl", "sphinx", "sphinx-notfound-page"] -idna = ["idna"] -mypy = ["idna", "mypy", "types-pyopenssl"] -tests = ["coverage[toml] (>=5.0.2)", "pytest"] - [[package]] name = "setuptools" version = "80.9.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" -groups = ["dev", "test"] +groups = ["test"] files = [ {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, @@ -2906,62 +2606,6 @@ outcome = ">=1.2.0" trio = ">=0.11" wsproto = ">=0.14" -[[package]] -name = "twisted" -version = "24.7.0" -description = "An asynchronous networking framework written in Python" -optional = false -python-versions = ">=3.8.0" -groups = ["dev"] -files = [ - {file = "twisted-24.7.0-py3-none-any.whl", hash = "sha256:734832ef98108136e222b5230075b1079dad8a3fc5637319615619a7725b0c81"}, - {file = "twisted-24.7.0.tar.gz", hash = "sha256:5a60147f044187a127ec7da96d170d49bcce50c6fd36f594e60f4587eff4d394"}, -] - -[package.dependencies] -attrs = ">=21.3.0" -automat = ">=0.8.0" -constantly = ">=15.1" -hyperlink = ">=17.1.1" -idna = {version = ">=2.4", optional = true, markers = "extra == \"tls\""} -incremental = ">=24.7.0" -pyopenssl = {version = ">=21.0.0", optional = true, markers = "extra == \"tls\""} -service-identity = {version = ">=18.1.0", optional = true, markers = "extra == \"tls\""} -typing-extensions = ">=4.2.0" -zope-interface = ">=5" - -[package.extras] -all-non-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226) ; platform_system == \"Windows\"", "pywin32 (!=226) ; platform_system == \"Windows\"", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)"] -conch = ["appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)"] -dev = ["coverage (>=7.5,<8.0)", "cython-test-exception-raiser (>=1.0.2,<2)", "hypothesis (>=6.56)", "pydoctor (>=23.9.0,<23.10.0)", "pyflakes (>=2.2,<3.0)", "pyhamcrest (>=2)", "python-subunit (>=1.4,<2.0)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "twistedchecker (>=0.7,<1.0)"] -dev-release = ["pydoctor (>=23.9.0,<23.10.0)", "pydoctor (>=23.9.0,<23.10.0)", "sphinx (>=6,<7)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "towncrier (>=23.6,<24.0)"] -gtk-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pygobject", "pygobject", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226) ; platform_system == \"Windows\"", "pywin32 (!=226) ; platform_system == \"Windows\"", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)"] -http2 = ["h2 (>=3.0,<5.0)", "priority (>=1.1.0,<2.0)"] -macos-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyobjc-core", "pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyobjc-framework-cocoa", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226) ; platform_system == \"Windows\"", "pywin32 (!=226) ; platform_system == \"Windows\"", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)"] -mypy = ["appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "coverage (>=7.5,<8.0)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "idna (>=2.4)", "mypy (>=1.8,<2.0)", "mypy-zope (>=1.0.3,<1.1.0)", "priority (>=1.1.0,<2.0)", "pydoctor (>=23.9.0,<23.10.0)", "pyflakes (>=2.2,<3.0)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "python-subunit (>=1.4,<2.0)", "pywin32 (!=226) ; platform_system == \"Windows\"", "service-identity (>=18.1.0)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "twistedchecker (>=0.7,<1.0)", "types-pyopenssl", "types-setuptools"] -osx-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyobjc-core", "pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyobjc-framework-cocoa", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226) ; platform_system == \"Windows\"", "pywin32 (!=226) ; platform_system == \"Windows\"", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)"] -serial = ["pyserial (>=3.0)", "pywin32 (!=226) ; platform_system == \"Windows\""] -test = ["cython-test-exception-raiser (>=1.0.2,<2)", "hypothesis (>=6.56)", "pyhamcrest (>=2)"] -tls = ["idna (>=2.4)", "pyopenssl (>=21.0.0)", "service-identity (>=18.1.0)"] -windows-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "pywin32 (!=226) ; platform_system == \"Windows\"", "pywin32 (!=226) ; platform_system == \"Windows\"", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)", "twisted-iocpsupport (>=1.0.2)", "twisted-iocpsupport (>=1.0.2)"] - -[[package]] -name = "txaio" -version = "23.1.1" -description = "Compatibility API between asyncio/Twisted/Trollius" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "txaio-23.1.1-py2.py3-none-any.whl", hash = "sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490"}, - {file = "txaio-23.1.1.tar.gz", hash = "sha256:f9a9216e976e5e3246dfd112ad7ad55ca915606b60b84a757ac769bd404ff704"}, -] - -[package.extras] -all = ["twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] -dev = ["pep8 (>=1.6.2)", "pyenchant (>=1.6.6)", "pytest (>=2.6.4)", "pytest-cov (>=1.8.1)", "sphinx (>=1.2.3)", "sphinx-rtd-theme (>=0.1.9)", "sphinxcontrib-spelling (>=2.1.2)", "tox (>=2.1.1)", "tox-gh-actions (>=2.2.0)", "twine (>=1.6.5)", "wheel"] -twisted = ["twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] - [[package]] name = "types-certifi" version = "2021.10.8.3" @@ -3202,64 +2846,6 @@ docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx_rtd_theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["pytest", "websockets"] -[[package]] -name = "websockets" -version = "10.3" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "websockets-10.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978"}, - {file = "websockets-10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500"}, - {file = "websockets-10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b"}, - {file = "websockets-10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c"}, - {file = "websockets-10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8"}, - {file = "websockets-10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677"}, - {file = "websockets-10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e"}, - {file = "websockets-10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f"}, - {file = "websockets-10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47"}, - {file = "websockets-10.3-cp310-cp310-win32.whl", hash = "sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae"}, - {file = "websockets-10.3-cp310-cp310-win_amd64.whl", hash = "sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079"}, - {file = "websockets-10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916"}, - {file = "websockets-10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb"}, - {file = "websockets-10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79"}, - {file = "websockets-10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d"}, - {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98"}, - {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e"}, - {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6"}, - {file = "websockets-10.3-cp37-cp37m-win32.whl", hash = "sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1"}, - {file = "websockets-10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4"}, - {file = "websockets-10.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36"}, - {file = "websockets-10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69"}, - {file = "websockets-10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd"}, - {file = "websockets-10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2"}, - {file = "websockets-10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c"}, - {file = "websockets-10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e"}, - {file = "websockets-10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991"}, - {file = "websockets-10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442"}, - {file = "websockets-10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76"}, - {file = "websockets-10.3-cp38-cp38-win32.whl", hash = "sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559"}, - {file = "websockets-10.3-cp38-cp38-win_amd64.whl", hash = "sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d"}, - {file = "websockets-10.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094"}, - {file = "websockets-10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667"}, - {file = "websockets-10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731"}, - {file = "websockets-10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9"}, - {file = "websockets-10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680"}, - {file = "websockets-10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247"}, - {file = "websockets-10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af"}, - {file = "websockets-10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3"}, - {file = "websockets-10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8"}, - {file = "websockets-10.3-cp39-cp39-win32.whl", hash = "sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582"}, - {file = "websockets-10.3-cp39-cp39-win_amd64.whl", hash = "sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02"}, - {file = "websockets-10.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7"}, - {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f"}, - {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4"}, - {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755"}, - {file = "websockets-10.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55"}, - {file = "websockets-10.3.tar.gz", hash = "sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4"}, -] - [[package]] name = "whitenoise" version = "6.2.0" @@ -3310,55 +2896,7 @@ enabler = ["pytest-enabler (>=2.2)"] test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] -[[package]] -name = "zope-interface" -version = "6.0" -description = "Interfaces for Python" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "zope.interface-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f299c020c6679cb389814a3b81200fe55d428012c5e76da7e722491f5d205990"}, - {file = "zope.interface-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee4b43f35f5dc15e1fec55ccb53c130adb1d11e8ad8263d68b1284b66a04190d"}, - {file = "zope.interface-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a158846d0fca0a908c1afb281ddba88744d403f2550dc34405c3691769cdd85"}, - {file = "zope.interface-6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f72f23bab1848edb7472309e9898603141644faec9fd57a823ea6b4d1c4c8995"}, - {file = "zope.interface-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48f4d38cf4b462e75fac78b6f11ad47b06b1c568eb59896db5b6ec1094eb467f"}, - {file = "zope.interface-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:87b690bbee9876163210fd3f500ee59f5803e4a6607d1b1238833b8885ebd410"}, - {file = "zope.interface-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2363e5fd81afb650085c6686f2ee3706975c54f331b426800b53531191fdf28"}, - {file = "zope.interface-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af169ba897692e9cd984a81cb0f02e46dacdc07d6cf9fd5c91e81f8efaf93d52"}, - {file = "zope.interface-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa90bac61c9dc3e1a563e5babb3fd2c0c1c80567e815442ddbe561eadc803b30"}, - {file = "zope.interface-6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89086c9d3490a0f265a3c4b794037a84541ff5ffa28bb9c24cc9f66566968464"}, - {file = "zope.interface-6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:809fe3bf1a91393abc7e92d607976bbb8586512913a79f2bf7d7ec15bd8ea518"}, - {file = "zope.interface-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:0ec9653825f837fbddc4e4b603d90269b501486c11800d7c761eee7ce46d1bbb"}, - {file = "zope.interface-6.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:790c1d9d8f9c92819c31ea660cd43c3d5451df1df61e2e814a6f99cebb292788"}, - {file = "zope.interface-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b39b8711578dcfd45fc0140993403b8a81e879ec25d53189f3faa1f006087dca"}, - {file = "zope.interface-6.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eba51599370c87088d8882ab74f637de0c4f04a6d08a312dce49368ba9ed5c2a"}, - {file = "zope.interface-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee934f023f875ec2cfd2b05a937bd817efcc6c4c3f55c5778cbf78e58362ddc"}, - {file = "zope.interface-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:042f2381118b093714081fd82c98e3b189b68db38ee7d35b63c327c470ef8373"}, - {file = "zope.interface-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dfbbbf0809a3606046a41f8561c3eada9db811be94138f42d9135a5c47e75f6f"}, - {file = "zope.interface-6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:424d23b97fa1542d7be882eae0c0fc3d6827784105264a8169a26ce16db260d8"}, - {file = "zope.interface-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e538f2d4a6ffb6edfb303ce70ae7e88629ac6e5581870e66c306d9ad7b564a58"}, - {file = "zope.interface-6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12175ca6b4db7621aedd7c30aa7cfa0a2d65ea3a0105393e05482d7a2d367446"}, - {file = "zope.interface-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3d7dfd897a588ec27e391edbe3dd320a03684457470415870254e714126b1f"}, - {file = "zope.interface-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b3f543ae9d3408549a9900720f18c0194ac0fe810cecda2a584fd4dca2eb3bb8"}, - {file = "zope.interface-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0583b75f2e70ec93f100931660328965bb9ff65ae54695fb3fa0a1255daa6f2"}, - {file = "zope.interface-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:23ac41d52fd15dd8be77e3257bc51bbb82469cf7f5e9a30b75e903e21439d16c"}, - {file = "zope.interface-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99856d6c98a326abbcc2363827e16bd6044f70f2ef42f453c0bd5440c4ce24e5"}, - {file = "zope.interface-6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1592f68ae11e557b9ff2bc96ac8fc30b187e77c45a3c9cd876e3368c53dc5ba8"}, - {file = "zope.interface-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4407b1435572e3e1610797c9203ad2753666c62883b921318c5403fb7139dec2"}, - {file = "zope.interface-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:5171eb073474a5038321409a630904fd61f12dd1856dd7e9d19cd6fe092cbbc5"}, - {file = "zope.interface-6.0.tar.gz", hash = "sha256:aab584725afd10c710b8f1e6e208dbee2d0ad009f57d674cb9d1b3964037275d"}, -] - -[package.dependencies] -setuptools = "*" - -[package.extras] -docs = ["Sphinx", "repoze.sphinx.autointerface"] -test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] -testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] - [metadata] lock-version = "2.1" python-versions = "~3.12.7" -content-hash = "7f80a46374e28adcc3734925d49e5f925b52f3c7b8d5495db2f712898b789364" +content-hash = "fa8c2452391b84212b3886ed714c6dd98d012c14af83f7ede3b01eea2525b872" diff --git a/backend/projectify/asgi.py b/backend/projectify/asgi.py index 850e9e6a6..3ed6cfe59 100644 --- a/backend/projectify/asgi.py +++ b/backend/projectify/asgi.py @@ -12,36 +12,15 @@ import os -from channels.auth import AuthMiddlewareStack -from channels.routing import ProtocolTypeRouter, URLRouter - from configurations.asgi import ( # type: ignore[attr-defined] get_asgi_application, ) -from projectify.middleware import CsrfTrustedOriginsOriginValidator # TODO still needed? We should just let the server crash when this env var is # unset os.environ.setdefault( "DJANGO_SETTINGS_MODULE", "projectify.settings.production" ) -asgi_application = get_asgi_application() - -# I believe we had to move this down here so that all applications can be -# mounted correctly, since importing the URLs could otherwise import views -# that use unitialized Django models. Justus 2023-10-18 -from .urls import websocket_urlpatterns # noqa: E402 - -websocket_application = CsrfTrustedOriginsOriginValidator( - AuthMiddlewareStack(URLRouter(websocket_urlpatterns)) -) - -application = ProtocolTypeRouter( - { - "http": asgi_application, - "websocket": websocket_application, - } -) - +application = get_asgi_application() __all__ = ("application",) diff --git a/backend/projectify/middleware.py b/backend/projectify/middleware.py index 253af975e..ac7b410ab 100644 --- a/backend/projectify/middleware.py +++ b/backend/projectify/middleware.py @@ -3,21 +3,12 @@ # SPDX-FileCopyrightText: 2022-2024 JWP Consulting GK """Projectify middlewares.""" -import asyncio import logging -import random from collections.abc import Awaitable from typing import Callable, Optional -from django.core.exceptions import ImproperlyConfigured -from django.core.handlers.asgi import ASGIHandler -from django.http import HttpRequest, HttpResponse, JsonResponse -from django.utils.decorators import async_only_middleware +from django.http import HttpRequest, HttpResponse -from channels.security.websocket import OriginValidator -from rest_framework import exceptions - -from projectify.lib.exception_handler import exception_handler from projectify.lib.settings import get_settings GetResponse = Callable[[HttpRequest], HttpResponse] @@ -87,90 +78,4 @@ def process_request(request: HttpRequest) -> HttpResponse: return process_request -def CsrfTrustedOriginsOriginValidator(application: ASGIHandler) -> ASGIHandler: - """Return an OriginValidator configured to use CSRF_TRUSTED_ORIGINS.""" - settings = get_settings() - origins = settings.CSRF_TRUSTED_ORIGINS - if origins is None: - raise ImproperlyConfigured( - "Need to specify CSRF_TRUSTED_ORIGINS in settings" - ) - return OriginValidator(application, origins) - - logger = logging.getLogger(__name__) - - -@async_only_middleware -def microsloth(get_response: AsyncGetResponse) -> AsyncGetResponse: - """Simulate a slow connection.""" - - async def process_request(request: HttpRequest) -> HttpResponse: - """Process the request.""" - response = await get_response(request) - if settings.SLEEP_MIN_MAX_MS is None: - return response - min, max = settings.SLEEP_MIN_MAX_MS - sleep_dur = random.randrange(min, max) / 1000 - await asyncio.sleep(sleep_dur) - return response - - return process_request - - -def errorsloth(get_response: GetResponse) -> GetResponse: - """Simulate an unreliable connection. Trigger error before response.""" - - def process_request(request: HttpRequest) -> HttpResponse: - """Process the request.""" - if settings.ERROR_RATE_PCT is None: - return get_response(request) - if random.randint(1, 100) <= settings.ERROR_RATE_PCT: - response = exception_handler( - exceptions.APIException("You have been slothed"), - {}, - ) - assert response - logger.info("Returning error error") - return JsonResponse( - data=response.data, status=response.status_code - ) - - return get_response(request) - - return process_request - - -class ErrorChannelator(ASGIHandler): - """ - Randomly interrupt requests while they are running. - - Useful for interrupting long websocket connections. - """ - - application: ASGIHandler - - def __init__(self, application: ASGIHandler): - """Initialize with application.""" - self.application = application - - async def __call__( - self, scope: object, receive: object, send: object - ) -> None: - """Wait a few seconds, then crash.""" - if settings.ERROR_RATE_PCT is None and settings.CHANNEL_ERROR is None: - return await self.application(scope, receive, send) - if settings.ERROR_RATE_PCT: - crash_start = random.randint(1, 100) <= settings.ERROR_RATE_PCT - if crash_start: - logger.info("Crashing channel before start") - raise Exception("Channelated") - if settings.CHANNEL_ERROR: - timeout = random.randint(1, settings.CHANNEL_ERROR) - try: - return await asyncio.wait_for( - self.application(scope, receive, send), timeout=timeout - ) - except TimeoutError as e: - logger.info("Crashing channel during connection") - raise Exception("Channelated") from e diff --git a/backend/projectify/settings/base.py b/backend/projectify/settings/base.py index 3467bbbd5..4ddec3cd0 100644 --- a/backend/projectify/settings/base.py +++ b/backend/projectify/settings/base.py @@ -26,12 +26,7 @@ from configurations import Configuration # type: ignore from .monkeypatch import patch -from .types import ( - ChannelLayers, - LoggingConfig, - StoragesConfig, - TemplatesConfig, -) +from .types import LoggingConfig, StoragesConfig, TemplatesConfig patch() @@ -120,7 +115,6 @@ class Base(Configuration): # type:ignore # Installed applications # Applications from Django project INSTALLED_APPS_DJANGO = ( - "channels", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", @@ -185,26 +179,9 @@ class Base(Configuration): # type:ignore WSGI_APPLICATION = "projectify.wsgi.application" ASGI_APPLICATION = "projectify.asgi.application" - # TODO remove when Svelte frontend is gone - # Channels - CHANNEL_LAYERS: ChannelLayers = { - "default": { - "BACKEND": "channels.layers.InMemoryChannelLayer", - }, - } - # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES: dict[str, dj_database_url.DBConfig] - # There was a reason why we added this - some weird issue - # with channels timing out. I am commenting this out temporarily. - # DATABASES["default"]["OPTIONS"] = { - # "options": ( - # "-c statement_timeout=5000 " - # "-c lock_timeout=5000 " - # "-c idle_in_transaction_session_timeout=5000 " - # ), - # } # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators @@ -354,14 +331,6 @@ class Base(Configuration): # type:ignore # premail PREMAIL_PREVIEW = False - # simulate slow and unreliable connections - SLEEP_MIN_MAX_MS: Optional[tuple[int, int]] = None - # Percentage (int from 0 to 100) of requests that should fail - ERROR_RATE_PCT: Optional[int] = None - # TODO remove when Svelte frontend is gone - # N seconds after which 100% of requests time out - CHANNEL_ERROR: Optional[int] = None - # tailwind # https://django-tailwind.readthedocs.io/en/latest/installation.html TAILWIND_APP_NAME = "projectify.theme" diff --git a/backend/projectify/settings/development.py b/backend/projectify/settings/development.py index 1c8b7206f..1d73345ed 100644 --- a/backend/projectify/settings/development.py +++ b/backend/projectify/settings/development.py @@ -29,8 +29,6 @@ def add_dev_middleware( yield m if debug_toolbar: yield "debug_toolbar.middleware.DebugToolbarMiddleware" - yield "projectify.middleware.microsloth" - yield "projectify.middleware.errorsloth" yield "django_browser_reload.middleware.BrowserReloadMiddleware" else: yield m @@ -146,12 +144,6 @@ class Development(SpectacularSettings, Base): "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } - # Settings for slow connection emulation - SLEEP_MIN_MAX_MS = 200, 500 - # ERROR_RATE_PCT = 20 - CHANNEL_ERROR = 20 - ASGI_APPLICATION = "projectify.test.asgi.error_application" - # Admins for local logging ADMINS = [["Local user", "user@localhost"]] diff --git a/backend/projectify/settings/production.py b/backend/projectify/settings/production.py index aca2fd242..3bf920ed1 100644 --- a/backend/projectify/settings/production.py +++ b/backend/projectify/settings/production.py @@ -5,9 +5,7 @@ import os import warnings -from collections.abc import Mapping from pathlib import Path -from typing import Any from projectify.lib.settings import populate_production_middleware @@ -28,40 +26,6 @@ def get_redis_tls_url() -> str: ) -def get_redis_channel_layer_hosts(redis_url: str) -> Mapping[str, Any]: - """ - Return django channels redis config. - - If rediss:// URL is given IGNORE ssl cert requirements. - - Both options are equally bad IMO. - """ - if redis_url.startswith("rediss://"): - warnings.warn( - "Initializing channels redis layer with TLS url and instructing the redis client to ignore SSL certificate requirements!", - ) - return { - "hosts": [ - { - "address": redis_url, - "ssl_cert_reqs": None, - } - ], - } - else: - warnings.warn( - "Initializing channels redis layer with non-TLS url and potentially" - "transmitting queries in clear text!", - ) - return { - "hosts": [ - { - "address": redis_url, - } - ], - } - - class Production(Base): """Production configuration.""" @@ -132,22 +96,6 @@ class Production(Base): STRIPE_PRICE_OBJECT = os.environ["STRIPE_PRICE_OBJECT"] STRIPE_ENDPOINT_SECRET = os.environ["STRIPE_ENDPOINT_SECRET"] - # REDIS - # https://devcenter.heroku.com/articles/connecting-heroku-redis#connecting-in-python - # Obviously, this isn't great - # https://github.com/django/channels_redis/issues/235 - # https://github.com/django/channels_redis/pull/337 - - CHANNEL_LAYERS = { - "default": { - "BACKEND": "channels_redis.core.RedisChannelLayer", - "CONFIG": { - **get_redis_channel_layer_hosts(REDIS_TLS_URL), - "symmetric_encryption_keys": [SECRET_KEY], - }, - }, - } - # Logging config LOGGING = Base.LOGGING LOGGING["handlers"]["mail_admins"] = { diff --git a/backend/projectify/settings/test.py b/backend/projectify/settings/test.py index 89f1c6986..5e5180430 100644 --- a/backend/projectify/settings/test.py +++ b/backend/projectify/settings/test.py @@ -27,9 +27,6 @@ class Test(SpectacularSettings, Base): # TODO populate me SECRET_KEY = "test" - # Allow localhost, so that websocket connections may pass - CSRF_TRUSTED_ORIGINS = ("http://localhost",) - FRONTEND_URL = "https://example.com" # Email @@ -67,20 +64,3 @@ class TestCollectstatic(Test): "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", }, } - - -class TestRedis(Test): - """ - Settings used to test the connection to Redis on localhost. - - See bin/test_redis.sh - """ - - CHANNEL_LAYERS = { - "default": { - "BACKEND": "channels_redis.core.RedisChannelLayer", - "CONFIG": { - "hosts": [("127.0.0.1", 6379)], - }, - }, - } diff --git a/backend/projectify/settings/types.py b/backend/projectify/settings/types.py index 30f26abd4..797b9c9b5 100644 --- a/backend/projectify/settings/types.py +++ b/backend/projectify/settings/types.py @@ -6,9 +6,6 @@ from collections.abc import Sequence from typing import Any, Mapping, TypedDict -ChannelLayer = Mapping[str, Any] -ChannelLayers = Mapping[str, ChannelLayer] - class TemplateConfig(TypedDict): """Configure one templating module.""" diff --git a/backend/projectify/test/asgi.py b/backend/projectify/test/asgi.py deleted file mode 100644 index 0c761e035..000000000 --- a/backend/projectify/test/asgi.py +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2024 JWP Consulting GK -"""Create asgi application optionally wrapped with debug middleware.""" - -from channels.routing import ProtocolTypeRouter - -from projectify.middleware import ErrorChannelator - -from ..asgi import asgi_application, websocket_application - -websocket_application = ErrorChannelator(websocket_application) - -error_application = ProtocolTypeRouter( - { - "http": asgi_application, - "websocket": websocket_application, - } -) diff --git a/backend/projectify/urls.py b/backend/projectify/urls.py index bdf4dd305..875c33b3d 100644 --- a/backend/projectify/urls.py +++ b/backend/projectify/urls.py @@ -20,8 +20,6 @@ from django.urls import URLPattern, URLResolver, include, path from django.views.generic import TemplateView -from projectify.workspace.consumers import ChangeConsumer - from .lib.settings import get_settings from .lib.views import ( colored_icon, @@ -142,9 +140,5 @@ ), ) -websocket_urlpatterns = ( - path("ws/workspace/change", ChangeConsumer.as_asgi()), -) - __all__ = ("handler404", "handler500", "handler403") diff --git a/backend/projectify/workspace/consumers.py b/backend/projectify/workspace/consumers.py deleted file mode 100644 index aec2114a8..000000000 --- a/backend/projectify/workspace/consumers.py +++ /dev/null @@ -1,389 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2022, 2023 JWP Consulting GK -"""Workspace ws consumers.""" - -import logging -from typing import ( - Any, - Literal, - NotRequired, - Optional, - TypedDict, - TypeVar, - Union, - cast, -) -from uuid import UUID - -from django.db import models - -from asgiref.sync import async_to_sync as _async_to_sync -from channels.generic.websocket import JsonWebsocketConsumer -from rest_framework import serializers, status - -from projectify.user.models import User - -from .models.project import Project -from .models.task import Task -from .models.workspace import Workspace -from .selectors.project import ( - ProjectDetailQuerySet, - project_find_by_project_uuid, -) -from .selectors.quota import workspace_get_all_quotas -from .selectors.task import TaskDetailQuerySet, task_find_by_task_uuid -from .selectors.workspace import ( - WorkspaceDetailQuerySet, - workspace_find_by_workspace_uuid, -) -from .serializers.project import ProjectDetailSerializer -from .serializers.task_detail import TaskDetailSerializer -from .serializers.workspace import WorkspaceDetailSerializer -from .types import ConsumerEvent, Resource - -logger = logging.getLogger(__name__) - -async_to_sync = cast(Any, _async_to_sync) - - -M = TypeVar("M", bound=models.Model) - - -# The below duplications are clunky -class ClientRequest(TypedDict): - """A message from a client to the ChangeConsumer.""" - - action: Literal["subscribe", "unsubscribe"] - resource: Literal["workspace", "project", "task"] - uuid: UUID - - -class ClientRequestSerializer(serializers.Serializer): - """Serializer for ClientRequest.""" - - action = serializers.ChoiceField(choices=["subscribe", "unsubscribe"]) - resource = serializers.ChoiceField( - choices=["workspace", "project", "task"] - ) - uuid = serializers.UUIDField() - - -class ClientResponse(TypedDict): - """An update to a resource.""" - - kind: Literal[ - "subscribed", - "unsubscribed", - "already_subscribed", - "not_subscribed", - "not_found", - "changed", - "gone", - ] - resource: Literal["workspace", "project", "task"] - uuid: UUID - content: NotRequired[object] - - -class ClientResponseSerializer(serializers.Serializer): - """Serializer for change.""" - - kind = serializers.ChoiceField( - choices=[ - "subscribed", - "unsubscribed", - "already_subscribed", - "not_subscribed", - "not_found", - "changed", - "gone", - ] - ) - resource = serializers.ChoiceField( - choices=["workspace", "project", "task"] - ) - uuid = serializers.UUIDField() - content = serializers.DictField(required=False) - - -def get_group_name(resource: Resource, uuid: UUID) -> str: - """Return the channel layer group name for a resource and uuid.""" - return f"{resource}-{uuid}" - - -ResourceInstance = Union[Workspace, Project, Task] - - -class ChangeConsumer(JsonWebsocketConsumer): - """Allow subscribing to changes to workspace resources.""" - - user: User - subscriptions: dict[UUID, Union[Workspace, Project, Task]] - - def connect(self) -> None: - """Handle connect.""" - self.subscriptions = {} - - self.user = self.scope["user"] - - if self.user.is_anonymous: - logger.warning("Anonymous user tried to connect") - self.close(status.HTTP_403_FORBIDDEN) - return - - self.accept() - - def is_subscribed_to(self, resource: Resource, uuid: UUID) -> bool: - """Return True if we are subscribed to a group.""" - sub = self.subscriptions.get(uuid) - match resource, sub: - case "workspace", Workspace(): - return True - case "project", Project(): - return True - case "task", Task(): - return True - case _: - return False - - T = TypeVar("T") - - def find_subscription( - self, resource: Resource, uuid: UUID - ) -> Optional[ResourceInstance]: - """Return True if we are subscribed to a group.""" - sub = self.subscriptions.get(uuid) - if sub is None: - return None - match resource, sub: - case "workspace", Workspace(): - return sub - case "project", Project(): - return sub - case "task", Task(): - return sub - case _: - raise ValueError( - f"Type mismatch for sub {sub} with {uuid}, expected {resource}" - ) - - def pop_subscription(self, resource: Resource, uuid: UUID) -> None: - """Pop a subscription, but only after type checking.""" - sub = self.find_subscription(resource, uuid) - if sub is None: - raise ValueError( - f"Can't pop, what has not been subscribed: {resource} {uuid}" - ) - self.subscriptions.pop(uuid) - - def add_subscription_for( - self, resource: Resource, uuid: UUID - ) -> Literal["not_found", "subscribed", "already_subscribed"]: - """Add a resource subscription.""" - who = self.user - inst: Optional[ResourceInstance] - match resource, self.is_subscribed_to(resource, uuid): - case "workspace", False: - inst = workspace_find_by_workspace_uuid( - who=who, workspace_uuid=uuid - ) - case "project", False: - inst = project_find_by_project_uuid(who=who, project_uuid=uuid) - case "task", False: - inst = task_find_by_task_uuid(who=self.user, task_uuid=uuid) - case _: - return "already_subscribed" - if inst is None: - return "not_found" - self.subscriptions[uuid] = inst - async_to_sync(self.channel_layer.group_add)( - get_group_name(resource, uuid), self.channel_name - ) - return "subscribed" - - def remove_subscription_for( - self, resource: Resource, uuid: UUID - ) -> Literal["not_subscribed", "unsubscribed"]: - """Remove a resource subscription.""" - if not self.is_subscribed_to(resource, uuid): - return "not_subscribed" - self.subscriptions.pop(uuid) - async_to_sync(self.channel_layer.group_discard)( - get_group_name(resource, uuid), self.channel_name - ) - return "unsubscribed" - - def remove_all_subscriptions(self) -> None: - """Remove all subscriptions, discard self from channel layer.""" - subs = list(self.subscriptions.items()) - for k, v in subs: - match v: - case Workspace(): - self.remove_subscription_for("workspace", k) - case Project(): - self.remove_subscription_for("project", k) - case Task(): - self.remove_subscription_for("task", k) - - def disconnect(self, close_code: int) -> None: - """Handle disconnect.""" - self.remove_all_subscriptions() - logger.debug("Disconnecting with code %d", close_code) - - def respond(self, response: ClientResponse) -> None: - """Respond to a client request.""" - serializer = ClientResponseSerializer(instance=response) - self.send_json(serializer.data) - - def receive_json(self, content: Any) -> None: - """Do nothing when receiving json.""" - serializer = ClientRequestSerializer(data=content) - if not serializer.is_valid(): - self.close(status.HTTP_400_BAD_REQUEST) - return - data = cast(ClientRequest, serializer.validated_data) - resource = data["resource"] - uuid = data["uuid"] - - result: Literal[ - "subscribed", - "not_found", - "not_subscribed", - "already_subscribed", - "unsubscribed", - ] - - match data["action"], resource: - case "subscribe", resource: - result = self.add_subscription_for(resource, uuid) - case "unsubscribe", resource: - result = self.remove_subscription_for(resource, uuid) - - response: ClientResponse - - match result: - case "already_subscribed": - logger.debug( - "Client was already subscribed to resource %s uuid %s", - resource, - uuid, - ) - response = { - "kind": "already_subscribed", - "resource": resource, - "uuid": uuid, - } - case "not_found": - logger.debug( - "No object found for uuid %s and resource %s", - uuid, - resource, - ) - response = { - "kind": "not_found", - "resource": resource, - "uuid": uuid, - } - case "not_subscribed": - logger.debug( - "Not subscribed to uuid %s and resource %s", uuid, resource - ) - response = { - "kind": "not_subscribed", - "resource": resource, - "uuid": uuid, - } - case "unsubscribed": - response = { - "kind": "unsubscribed", - "resource": resource, - "uuid": uuid, - } - case "subscribed": - response = { - "kind": "subscribed", - "resource": resource, - "uuid": uuid, - } - self.respond(response) - - def change(self, event: ConsumerEvent) -> None: - """Respond to project change event.""" - # Check if already subscribed - uuid = UUID(event["uuid"]) - response: ClientResponse - sub = self.find_subscription(event["resource"], uuid) - result: Union[ - Literal["gone", "not_found", "never_subscribed"], - serializers.Serializer, - ] - match event["kind"], sub: - case "gone", _: - result = "gone" - case "changed", Workspace() as w: - workspace = workspace_find_by_workspace_uuid( - who=self.user, - workspace_uuid=w.uuid, - qs=WorkspaceDetailQuerySet, - ) - if workspace is not None: - workspace.quota = workspace_get_all_quotas(workspace) - result = WorkspaceDetailSerializer(workspace) - else: - result = "not_found" - case "changed", Project() as p: - project = project_find_by_project_uuid( - who=self.user, - project_uuid=p.uuid, - qs=ProjectDetailQuerySet, - ) - if project is not None: - project.workspace.quota = workspace_get_all_quotas( - project.workspace - ) - result = ProjectDetailSerializer(project) - else: - result = "not_found" - case "changed", Task() as t: - task = task_find_by_task_uuid( - who=self.user, task_uuid=t.uuid, qs=TaskDetailQuerySet - ) - if task is not None: - result = TaskDetailSerializer(task) - else: - result = "not_found" - case "changed", None: - result = "never_subscribed" - - match result: - case "gone": - self.remove_subscription_for(event["resource"], uuid) - response = { - "kind": "gone", - "resource": event["resource"], - "uuid": uuid, - } - case "not_found": - self.remove_subscription_for(event["resource"], uuid) - response = { - "kind": "gone", - "resource": event["resource"], - "uuid": uuid, - } - case "never_subscribed": - logger.warning( - "Received update for resource %s and uuid %s" - "despite never having subscribed", - event["resource"], - uuid, - ) - return - case _: - response = { - "kind": "changed", - "resource": event["resource"], - "uuid": uuid, - "content": result.data, - } - self.respond(response) diff --git a/backend/projectify/workspace/services/chat_message.py b/backend/projectify/workspace/services/chat_message.py index 0ebd5de4b..064781c0f 100644 --- a/backend/projectify/workspace/services/chat_message.py +++ b/backend/projectify/workspace/services/chat_message.py @@ -14,7 +14,6 @@ from projectify.workspace.selectors.team_member import ( team_member_find_for_workspace, ) -from projectify.workspace.services.signals import send_change_signal # TODO this could take an author instead of who -> user is derived from author @@ -33,5 +32,4 @@ def chat_message_create( instance = ChatMessage.objects.create( task=task, text=text, author=team_member ) - send_change_signal("changed", task) return instance diff --git a/backend/projectify/workspace/services/label.py b/backend/projectify/workspace/services/label.py index 8c44334dd..dde466c87 100644 --- a/backend/projectify/workspace/services/label.py +++ b/backend/projectify/workspace/services/label.py @@ -7,7 +7,6 @@ from projectify.user.models import User from projectify.workspace.models.label import Label from projectify.workspace.models.workspace import Workspace -from projectify.workspace.services.signals import send_change_signal # Create @@ -23,7 +22,6 @@ def label_create( """Create a label.""" validate_perm("workspace.create_label", who, workspace) label = Label.objects.create(workspace=workspace, name=name, color=color) - send_change_signal("changed", workspace) return label @@ -41,7 +39,6 @@ def label_update( label.name = name label.color = color label.save() - send_change_signal("changed", label.workspace) return label @@ -51,4 +48,3 @@ def label_delete(*, who: User, label: Label) -> None: """Delete a label.""" validate_perm("workspace.delete_label", who, label.workspace) label.delete() - send_change_signal("changed", label.workspace) diff --git a/backend/projectify/workspace/services/project.py b/backend/projectify/workspace/services/project.py index ea125f7ed..3a20ca269 100644 --- a/backend/projectify/workspace/services/project.py +++ b/backend/projectify/workspace/services/project.py @@ -12,7 +12,6 @@ from projectify.user.models import User from projectify.workspace.models import Project from projectify.workspace.models.workspace import Workspace -from projectify.workspace.services.signals import send_change_signal # Create @@ -31,7 +30,6 @@ def project_create( title=title, description=description, due_date=due_date ) # 1+1 query? - send_change_signal("changed", project.workspace) return project @@ -53,9 +51,6 @@ def project_update( raise ValueError(f"tzinfo must be specified, got {due_date}") project.due_date = due_date project.save() - # 1 + 1 query performance problem ? - send_change_signal("changed", project.workspace) - send_change_signal("changed", project) return project @@ -65,9 +60,6 @@ def project_delete(*, who: User, project: Project) -> None: """Delete a project.""" validate_perm("workspace.delete_project", who, project.workspace) project.delete() - # 1 + 1 query performance problem ? - send_change_signal("changed", project.workspace) - send_change_signal("gone", project) # RPC @@ -80,7 +72,4 @@ def project_archive(*, who: User, project: Project, archived: bool) -> Project: else: project.archived = None project.save() - # 1 + 1 query performance problem ? - send_change_signal("changed", project.workspace) - send_change_signal("gone", project) return project diff --git a/backend/projectify/workspace/services/section.py b/backend/projectify/workspace/services/section.py index bb23ab3f0..b0aa8a7d1 100644 --- a/backend/projectify/workspace/services/section.py +++ b/backend/projectify/workspace/services/section.py @@ -10,7 +10,6 @@ from projectify.lib.auth import validate_perm from projectify.user.models import User from projectify.workspace.models import Project, Section -from projectify.workspace.services.signals import send_change_signal # Create @@ -30,7 +29,6 @@ def section_create( ) section = Section(title=title, description=description, project=project) section.save() - send_change_signal("changed", project) return section @@ -52,7 +50,6 @@ def section_update( section.title = title section.description = description section.save() - send_change_signal("changed", section.project) return section @@ -70,7 +67,6 @@ def section_delete( section.project.workspace, ) section.delete() - send_change_signal("changed", section.project) @transaction.atomic @@ -121,7 +117,6 @@ def section_move( # Set new order project.set_section_order(order_list) project.save() - send_change_signal("changed", section.project) def section_minimize(*, who: User, section: Section, minimized: bool) -> None: diff --git a/backend/projectify/workspace/services/signals.py b/backend/projectify/workspace/services/signals.py deleted file mode 100644 index 9a7fdbe54..000000000 --- a/backend/projectify/workspace/services/signals.py +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2024 JWP Consulting GK -"""Functions to handle signals.""" - -from typing import Any, Literal, Union, cast - -from asgiref.sync import async_to_sync as _async_to_sync -from channels.layers import get_channel_layer - -from ..models.project import Project -from ..models.task import Task -from ..models.workspace import Workspace -from ..types import ConsumerEvent, Resource - -# TODO AsyncToSync is typed in a newer (unreleased) version of asgiref -# which we indirectly install with channels, which has not been -# renewed in a while Justus 2023-05-19 -async_to_sync = cast(Any, _async_to_sync) - - -def send_change_signal( - kind: Literal["changed", "gone"], object: Union[Workspace, Project, Task] -) -> None: - """Send a change signal to the correct channels layer group.""" - resource: Resource - match object: - case Workspace(): - group = f"workspace-{object.uuid}" - resource = "workspace" - case Project(): - group = f"project-{object.uuid}" - resource = "project" - case Task(): - group = f"task-{object.uuid}" - resource = "task" - event: ConsumerEvent = { - "type": "change", - "resource": resource, - "uuid": str(object.uuid), - "kind": kind, - } - channel_layer = get_channel_layer() - if not channel_layer: - raise Exception("Did not get channel layer") - async_to_sync(channel_layer.group_send)(group, event) diff --git a/backend/projectify/workspace/services/sub_task.py b/backend/projectify/workspace/services/sub_task.py index 4511d050e..44432004a 100644 --- a/backend/projectify/workspace/services/sub_task.py +++ b/backend/projectify/workspace/services/sub_task.py @@ -12,7 +12,6 @@ from projectify.user.models import User from projectify.workspace.models.sub_task import SubTask from projectify.workspace.models.task import Task -from projectify.workspace.services.signals import send_change_signal class ValidatedDatum(TypedDict): @@ -40,12 +39,6 @@ class ValidatedData(TypedDict): update_sub_tasks: Optional[Sequence[ValidatedDatumWithUuid]] -def _sub_task_changed(task: Task) -> None: - """Broadcast changes upon sub task save/delete.""" - send_change_signal("changed", task.section.project) - send_change_signal("changed", task) - - @transaction.atomic def sub_task_create( *, @@ -60,7 +53,6 @@ def sub_task_create( sub_task = SubTask.objects.create( task=task, title=title, description=description, done=done ) - _sub_task_changed(task) return sub_task @@ -77,7 +69,6 @@ def sub_task_create_many( sub_tasks: list[SubTask] = SubTask.objects.bulk_create( SubTask(task=task, **sub_task) for sub_task in create_sub_tasks ) - _sub_task_changed(task) return sub_tasks @@ -148,5 +139,4 @@ def sub_task_update_many( result += create_instances # 4) fix order # XXX ? is fix order missing? - _sub_task_changed(task) return result diff --git a/backend/projectify/workspace/services/task.py b/backend/projectify/workspace/services/task.py index 9973f064c..08a9e521c 100644 --- a/backend/projectify/workspace/services/task.py +++ b/backend/projectify/workspace/services/task.py @@ -20,7 +20,6 @@ from ..models.section import Section from ..models.task import Task from ..models.team_member import TeamMember -from ..services.signals import send_change_signal from ..services.sub_task import ( ValidatedData, sub_task_create_many, @@ -121,7 +120,6 @@ def task_create_nested( task=task, create_sub_tasks=create_sub_tasks, ) - send_change_signal("changed", task.section.project) return task @@ -156,8 +154,6 @@ def task_update_nested( create_sub_tasks=sub_tasks["create_sub_tasks"] or [], update_sub_tasks=sub_tasks["update_sub_tasks"] or [], ) - send_change_signal("changed", task.section.project) - send_change_signal("changed", task) return task @@ -167,8 +163,6 @@ def task_delete(*, task: Task, who: User) -> None: """Delete a task.""" validate_perm("workspace.delete_task", who, task.workspace) task.delete() - send_change_signal("changed", task.section.project) - send_change_signal("gone", task) @transaction.atomic @@ -250,6 +244,4 @@ def task_move_after( # Set the order section.set_task_order(order_list) section.save() - send_change_signal("changed", task.section.project) - send_change_signal("changed", task) return task diff --git a/backend/projectify/workspace/services/team_member.py b/backend/projectify/workspace/services/team_member.py index 92cda92de..9104a3ccf 100644 --- a/backend/projectify/workspace/services/team_member.py +++ b/backend/projectify/workspace/services/team_member.py @@ -17,7 +17,6 @@ from ..models.const import TeamMemberRoles from ..models.project import Project from ..models.team_member import TeamMember -from .signals import send_change_signal @transaction.atomic @@ -28,7 +27,6 @@ def team_member_update( validate_perm("workspace.update_team_member", who, team_member.workspace) team_member.job_title = job_title team_member.save() - send_change_signal("changed", team_member.workspace) return team_member @@ -40,7 +38,6 @@ def team_member_change_role( validate_perm("workspace.update_team_member_role", who, team_member) team_member.role = role team_member.save() - send_change_signal("changed", team_member.workspace) return team_member @@ -64,7 +61,6 @@ def team_member_delete(*, team_member: TeamMember, who: User) -> None: _("You can't remove yourself from this workspace") ) team_member.delete() - send_change_signal("changed", team_member.workspace) def team_member_visit_workspace(*, team_member: TeamMember) -> None: diff --git a/backend/projectify/workspace/services/team_member_invite.py b/backend/projectify/workspace/services/team_member_invite.py index a5a6b6277..a0ddb662a 100644 --- a/backend/projectify/workspace/services/team_member_invite.py +++ b/backend/projectify/workspace/services/team_member_invite.py @@ -14,7 +14,6 @@ from projectify.premail.email import EmailAddress from projectify.user.models import User, UserInvite from projectify.user.services.user_invite import user_invite_create -from projectify.workspace.services.signals import send_change_signal from ..emails import TeamMemberInviteEmail from ..models.const import TeamMemberRoles @@ -155,7 +154,6 @@ def team_member_invite_create( ) email_to_send.send() - send_change_signal("changed", workspace) return team_member_invite @@ -176,4 +174,3 @@ def team_member_invite_delete( ) case TeamMemberInvite() as team_member_invite: team_member_invite.delete() - send_change_signal("changed", workspace) diff --git a/backend/projectify/workspace/services/workspace.py b/backend/projectify/workspace/services/workspace.py index 61978c00f..13a6f83c4 100644 --- a/backend/projectify/workspace/services/workspace.py +++ b/backend/projectify/workspace/services/workspace.py @@ -19,7 +19,6 @@ from projectify.corporate.services.customer import customer_create from projectify.lib.auth import validate_perm from projectify.user.models import User -from projectify.workspace.services.signals import send_change_signal from ..models.const import TeamMemberRoles from ..models.team_member import TeamMember @@ -76,7 +75,6 @@ def workspace_update( case picture: workspace.picture = picture workspace.save() - send_change_signal("changed", workspace) return workspace @@ -117,7 +115,6 @@ def workspace_delete( workspace, count, ) - send_change_signal("gone", workspace) workspace.delete() @@ -131,5 +128,4 @@ def workspace_add_user( ) -> TeamMember: """Add user to workspace. Return new team member.""" team_member = workspace.teammember_set.create(user=user, role=role) - send_change_signal("changed", workspace) return team_member diff --git a/backend/projectify/workspace/test/test_consumers.py b/backend/projectify/workspace/test/test_consumers.py deleted file mode 100644 index 0d0659f6f..000000000 --- a/backend/projectify/workspace/test/test_consumers.py +++ /dev/null @@ -1,732 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2022-2024 JWP Consulting GK -"""Consumer tests.""" - -# TODO -# - replace .disconnect() calls with clean_up_communicator -# - put instance .delete() calls in each fixture -import logging -from collections.abc import AsyncIterable -from typing import Any, Union, cast -from unittest import mock - -from django.contrib.auth.models import AnonymousUser - -import pytest -from channels.db import database_sync_to_async -from channels.testing import WebsocketCommunicator - -from projectify.asgi import websocket_application -from projectify.corporate.services.stripe import customer_activate_subscription -from projectify.user.models import User -from projectify.user.models.user_invite import UserInvite -from projectify.user.services.internal import user_create -from projectify.workspace.consumers import ( - ClientResponse, - ClientResponseSerializer, -) - -from ..models.const import TeamMemberRoles -from ..models.label import Label -from ..models.project import Project -from ..models.section import Section -from ..models.task import Task -from ..models.team_member import TeamMember -from ..models.workspace import Workspace -from ..selectors.team_member import team_member_find_for_workspace -from ..services.chat_message import chat_message_create -from ..services.label import label_create, label_delete, label_update -from ..services.project import ( - project_archive, - project_create, - project_delete, - project_update, -) -from ..services.section import ( - section_create, - section_delete, - section_move, - section_update, -) -from ..services.sub_task import sub_task_update_many -from ..services.task import ( - task_create, - task_create_nested, - task_delete, - task_move_after, - task_update_nested, -) -from ..services.team_member import ( - team_member_change_role, - team_member_delete, - team_member_update, -) -from ..services.team_member_invite import ( - team_member_invite_create, - team_member_invite_delete, -) -from ..services.workspace import ( - workspace_create, - workspace_delete, - workspace_update, -) - -logger = logging.getLogger(__name__) - - -@pytest.fixture -async def user() -> AsyncIterable[User]: - """Create a user.""" - user = await database_sync_to_async(user_create)( - email="consumer-test-1@example.com" - ) - yield user - # TODO use a service based user deletion here - await database_sync_to_async(user.delete)() - - -@pytest.fixture -async def other_user() -> AsyncIterable[User]: - """Create another user.""" - user = await database_sync_to_async(user_create)( - email="consumer-test-2@example.com" - ) - yield user - # TODO use a service based user deletion here - await database_sync_to_async(user.delete)() - - -@pytest.fixture -async def workspace(user: User) -> AsyncIterable[Workspace]: - """Create a paid for workspace.""" - workspace = await database_sync_to_async(workspace_create)( - title="Workspace title", - owner=user, - ) - customer = workspace.customer - # XXX use same fixture as in corporate/test/conftest.py - await database_sync_to_async(customer_activate_subscription)( - customer=customer, - stripe_customer_id="stripe_", - seats=10, - ) - yield workspace - await database_sync_to_async(workspace_delete)( - who=user, workspace=workspace - ) - - -@pytest.fixture -async def team_member(workspace: Workspace, user: User) -> TeamMember: - """Return team member with owner status.""" - team_member = await database_sync_to_async(team_member_find_for_workspace)( - workspace=workspace, user=user - ) - assert team_member - team_member.user = user - return team_member - # No delete necessary, it will be deleted as part of the workspace - - -@pytest.fixture -async def project( - workspace: Workspace, team_member: TeamMember -) -> AsyncIterable[Project]: - """Create project.""" - project = await database_sync_to_async(project_create)( - who=team_member.user, title="Don't care", workspace=workspace - ) - yield project - await database_sync_to_async(project_delete)( - who=team_member.user, project=project - ) - - -@pytest.fixture -async def section( - project: Project, team_member: TeamMember -) -> AsyncIterable[Section]: - """Create section.""" - section = await database_sync_to_async(section_create)( - project=project, who=team_member.user, title="I am a section" - ) - yield section - await database_sync_to_async(section_delete)( - who=team_member.user, section=section - ) - - -@pytest.fixture -async def task( - section: Section, - team_member: TeamMember, -) -> AsyncIterable[Task]: - """Create task.""" - task = await database_sync_to_async(task_create)( - section=section, - who=team_member.user, - assignee=team_member, - title="I am a task", - ) - yield task - await database_sync_to_async(task_delete)(who=team_member.user, task=task) - - -@pytest.fixture -async def label( - workspace: Workspace, team_member: TeamMember -) -> AsyncIterable[Label]: - """Create a label.""" - label = await database_sync_to_async(label_create)( - workspace=workspace, who=team_member.user, color=0, name="don't care" - ) - yield label - await database_sync_to_async(label_delete)( - who=team_member.user, label=label - ) - - -HasUuid = Union[Workspace, Project, Task] - - -async def expect_change( - communicator: WebsocketCommunicator, has_uuid: HasUuid -) -> Any: - """Test if the message is correct.""" - match has_uuid: - case Workspace(): - resource = "workspace" - case Project(): - resource = "project" - case Task(): - resource = "task" - json = await communicator.receive_json_from() - serializer = ClientResponseSerializer(data=json) - serializer.is_valid(raise_exception=True) - json_cast = cast(ClientResponse, serializer.validated_data) - logger.info("Received message %s for %s", json_cast["resource"], has_uuid) - assert json_cast == { - "kind": "changed", - "uuid": has_uuid.uuid, - "resource": resource, - "content": mock.ANY, - } - content = json_cast.get("content") - assert content is not None, json_cast - return content - - -async def expect_gone( - communicator: WebsocketCommunicator, resource: HasUuid -) -> None: - """Test if gone response is received.""" - response = await communicator.receive_json_from() - serializer = ClientResponseSerializer(data=response) - serializer.is_valid(raise_exception=True) - data = cast(ClientResponse, serializer.validated_data) - assert data == { - "kind": "gone", - "uuid": resource.uuid, - "resource": mock.ANY, - } - - -pytestmark = [pytest.mark.django_db, pytest.mark.asyncio] - - -async def make_communicator( - resource: Union[Workspace, Project, Task], user: Union[User, AnonymousUser] -) -> WebsocketCommunicator: - """Create a websocket communicator for a given resource and user.""" - match resource: - case Workspace(): - resource_str = "workspace" - case Project(): - resource_str = "project" - case Task(): - resource_str = "task" - communicator = WebsocketCommunicator( - websocket_application, "ws/workspace/change" - ) - communicator.scope["user"] = user - headers = communicator.scope.get("headers", []) - communicator.scope["headers"] = [ - *headers, - [b"origin", b"http://localhost"], - ] - connected, code = await communicator.connect() - if connected is False: - await communicator.disconnect() - raise Exception(f"Not connected: {code}") - await communicator.send_json_to( - { - "action": "subscribe", - "resource": resource_str, - "uuid": str(resource.uuid), - } - ) - response = await communicator.receive_json_from() - serializer = ClientResponseSerializer(data=response) - serializer.is_valid(raise_exception=True) - data = cast(ClientResponse, serializer.validated_data) - if data["kind"] != "subscribed": - await clean_up_communicator(communicator) - raise Exception(f"Could not connect, {data['kind']}") - assert data == { - "kind": "subscribed", - "uuid": resource.uuid, - "resource": resource_str, - }, data - return communicator - - -async def clean_up_communicator(communicator: WebsocketCommunicator) -> None: - """Clean up a communicator.""" - assert ( - await communicator.receive_nothing() is True - ), "There was another extra message" - await communicator.disconnect() - - -@pytest.fixture -async def workspace_communicator( - workspace: Workspace, user: User -) -> AsyncIterable[WebsocketCommunicator]: - """Return a communicator to a workspace instance.""" - communicator = await make_communicator(workspace, user) - yield communicator - await clean_up_communicator(communicator) - - -@pytest.fixture -async def project_communicator( - project: Project, user: User -) -> AsyncIterable[WebsocketCommunicator]: - """Return a communicator to a project instance.""" - communicator = await make_communicator(project, user) - yield communicator - await clean_up_communicator(communicator) - - -@pytest.fixture -async def task_communicator( - task: Task, user: User -) -> AsyncIterable[WebsocketCommunicator]: - """Return a communicator to a task instance.""" - communicator = await make_communicator(task, user) - yield communicator - await clean_up_communicator(communicator) - - -class TestWorkspace: - """Test consumer behavior for Workspace changes.""" - - async def test_not_found( - self, workspace: Workspace, other_user: User - ) -> None: - """Test we can't connect to an unrelated workspace's consumer.""" - with pytest.raises(Exception) as e: - await make_communicator(workspace, AnonymousUser()) - assert e.match("Not connected: 403") - with pytest.raises(Exception) as e: - await make_communicator(workspace, other_user) - assert e.match("Could not connect, not_found") - - async def test_workspace_life_cycle( - self, - team_member: TeamMember, - ) -> None: - """Test signal firing on workspace change.""" - workspace = await database_sync_to_async(workspace_create)( - owner=team_member.user, title="A workspace" - ) - - workspace_communicator = await make_communicator( - workspace, team_member.user - ) - - await database_sync_to_async(workspace_update)( - workspace=workspace, - who=team_member.user, - title="A new hope", - picture=None, - ) - assert await expect_change(workspace_communicator, workspace) - await database_sync_to_async(workspace_delete)( - who=team_member.user, workspace=workspace - ) - await expect_gone(workspace_communicator, workspace) - await clean_up_communicator(workspace_communicator) - - -class TestTeamMember: - """Test consumer behavior for TeamMember changes.""" - - async def test_team_member_life_cycle( - self, - other_user: User, - workspace: Workspace, - team_member: TeamMember, - workspace_communicator: WebsocketCommunicator, - ) -> None: - """Test signal firing on team member save or delete.""" - # New team member - other_team_member = await database_sync_to_async( - team_member_invite_create - )(workspace=workspace, email_or_user=other_user, who=team_member.user) - assert isinstance(other_team_member, TeamMember) - await expect_change(workspace_communicator, workspace) - # Team member updated - await database_sync_to_async(team_member_update)( - team_member=other_team_member, - who=team_member.user, - job_title="asd", - ) - await expect_change(workspace_communicator, workspace) - - await database_sync_to_async(team_member_change_role)( - team_member=other_team_member, - who=team_member.user, - role=TeamMemberRoles.OWNER, - ) - await expect_change(workspace_communicator, workspace) - - # TODO user updated (picture/name) - - # Team member deleted (delete initial ws user as well) - await database_sync_to_async(team_member_delete)( - team_member=other_team_member, who=team_member.user - ) - await expect_change(workspace_communicator, workspace) - - # Now we invite someone without an account: - await database_sync_to_async(team_member_invite_create)( - workspace=workspace, - email_or_user="doesnotexist@example.com", - who=team_member.user, - ) - await expect_change(workspace_communicator, workspace) - - # And we remove their invitation - await database_sync_to_async(team_member_invite_delete)( - workspace=workspace, - email="doesnotexist@example.com", - who=team_member.user, - ) - await expect_change(workspace_communicator, workspace) - - # Clean up user invite - await UserInvite.objects.all().adelete() - - -class TestProject: - """Test consumer behavior for Project changes.""" - - async def test_not_found(self, other_user: User, project: Project) -> None: - """Test we can't connect to an unrelated project's consumer.""" - with pytest.raises(Exception) as e: - await make_communicator(project, AnonymousUser()) - assert e.match("Not connected: 403") - with pytest.raises(Exception) as e: - await make_communicator(project, other_user) - assert e.match("Could not connect, not_found") - - async def test_project_life_cycle( - self, - workspace: Workspace, - team_member: TeamMember, - workspace_communicator: WebsocketCommunicator, - ) -> None: - """Test workspace / board consumer behavior for board changes.""" - # Create - project = await database_sync_to_async(project_create)( - who=team_member.user, - workspace=workspace, - title="It's time to chew bubble gum and write Django", - description="And I'm all out of Django", - ) - assert await expect_change(workspace_communicator, workspace) - - project_communicator = await make_communicator( - project, team_member.user - ) - - # Update - await database_sync_to_async(project_update)( - who=team_member.user, project=project, title="don't care" - ) - project_new = await expect_change(project_communicator, project) - assert project_new["workspace"]["quota"] is not None - assert await expect_change(workspace_communicator, workspace) - - # Archive - await database_sync_to_async(project_archive)( - who=team_member.user, project=project, archived=True - ) - assert await expect_change(workspace_communicator, workspace) - - # Delete - await database_sync_to_async(project_delete)( - who=team_member.user, project=project - ) - await expect_gone(project_communicator, project) - assert await expect_change(workspace_communicator, workspace) - - await clean_up_communicator(project_communicator) - - -class TestSection: - """Test section behavior.""" - - async def test_section_life_cycle( - self, - team_member: TeamMember, - project: Project, - project_communicator: WebsocketCommunicator, - ) -> None: - """Test project consumer behavior for section changes.""" - # Create it - section = await database_sync_to_async(section_create)( - who=team_member.user, title="A section", project=project - ) - assert await expect_change(project_communicator, project) - - # Update it - await database_sync_to_async(section_update)( - who=team_member.user, section=section, title="Title has changed" - ) - assert await expect_change(project_communicator, project) - - # Move it - await database_sync_to_async(section_move)( - who=team_member.user, section=section, order=0 - ) - assert await expect_change(project_communicator, project) - - # Delete it - await database_sync_to_async(section_delete)( - who=team_member.user, section=section - ) - assert await expect_change(project_communicator, project) - - await project_communicator.disconnect() - - -class TestLabel: - """Test consumer behavior for label changes.""" - - async def test_label_life_cycle( - self, - workspace: Workspace, - team_member: TeamMember, - workspace_communicator: WebsocketCommunicator, - ) -> None: - """Test that workspace consumer fires on label changes.""" - # Create - label = await database_sync_to_async(label_create)( - who=team_member.user, workspace=workspace, color=0, name="hello" - ) - assert await expect_change(workspace_communicator, workspace) - # Update - await database_sync_to_async(label_update)( - who=team_member.user, label=label, color=1, name="updated" - ) - assert await expect_change(workspace_communicator, workspace) - - # Delete - await database_sync_to_async(label_delete)( - who=team_member.user, label=label - ) - assert await expect_change(workspace_communicator, workspace) - await workspace_communicator.disconnect() - - -class TestTask: - """Test consumer behavior for tasks.""" - - async def test_not_found(self, other_user: User, task: Task) -> None: - """Test we can't connect to an unrelated task's consumer.""" - with pytest.raises(Exception) as e: - await make_communicator(task, AnonymousUser()) - assert e.match("Not connected: 403") - with pytest.raises(Exception) as e: - await make_communicator(task, other_user) - assert e.match("Could not connect, not_found") - - async def test_task_life_cycle( - self, - team_member: TeamMember, - project: Project, - section: Section, - project_communicator: WebsocketCommunicator, - ) -> None: - """Test that board and task consumer fire.""" - # Create - task = await database_sync_to_async(task_create_nested)( - who=team_member.user, - section=section, - title="A task", - sub_tasks={"create_sub_tasks": [], "update_sub_tasks": []}, - labels=[], - ) - assert await expect_change(project_communicator, project) - - task_communicator = await make_communicator(task, team_member.user) - - # Update - await database_sync_to_async(task_update_nested)( - who=team_member.user, - task=task, - title="A task", - sub_tasks={"create_sub_tasks": [], "update_sub_tasks": []}, - labels=[], - ) - assert await expect_change(project_communicator, project) - assert await expect_change(task_communicator, task) - - # Move - await database_sync_to_async(task_move_after)( - who=team_member.user, - task=task, - after=section, - ) - assert await expect_change(project_communicator, project) - assert await expect_change(task_communicator, task) - - # Delete - await database_sync_to_async(task_delete)( - who=team_member.user, - task=task, - ) - await expect_gone(task_communicator, task) - await expect_gone(task_communicator, task) - # XXX project signal fires three times here, for some reason - assert await expect_change(project_communicator, project) - assert await expect_change(project_communicator, project) - assert await expect_change(project_communicator, project) - - # Ideally, a task consumer will disconnect when a task is deleted - await clean_up_communicator(task_communicator) - - -class TestTaskLabel: - """Test consumer behavior for task labels.""" - - async def test_label_added_or_removed( - self, - team_member: TeamMember, - label: Label, - project: Project, - task: Task, - project_communicator: WebsocketCommunicator, - task_communicator: WebsocketCommunicator, - ) -> None: - """Test that project and task consumer fire.""" - # Add label - await database_sync_to_async(task_update_nested)( - who=team_member.user, - task=task, - title=task.title, - labels=[label], - sub_tasks={"create_sub_tasks": [], "update_sub_tasks": []}, - ) - assert await expect_change(project_communicator, project) - assert await expect_change(task_communicator, task) - - # Remove label - await database_sync_to_async(task_update_nested)( - who=team_member.user, - task=task, - title=task.title, - labels=[], - sub_tasks={"create_sub_tasks": [], "update_sub_tasks": []}, - ) - assert await expect_change(task_communicator, task) - # XXX There are extra messages, for some reason - assert await expect_change(task_communicator, task) - assert await expect_change(task_communicator, task) - # XXX there are extra messages here - assert await expect_change(project_communicator, project) - assert await expect_change(project_communicator, project) - assert await expect_change(project_communicator, project) - - await project_communicator.disconnect() - await task_communicator.disconnect() - - -class TestSubTask: - """Test consumer behavior for sub tasks.""" - - async def test_sub_task_saved_or_deleted_project( - self, - team_member: TeamMember, - project: Project, - task: Task, - project_communicator: WebsocketCommunicator, - task_communicator: WebsocketCommunicator, - ) -> None: - """Test that project and task consumer fire.""" - # Simulate adding a task - (sub_task,) = await database_sync_to_async(sub_task_update_many)( - who=team_member.user, - task=task, - sub_tasks=[], - create_sub_tasks=[ - {"title": "to do", "done": False, "_order": 0}, - ], - update_sub_tasks=[], - ) - # Simulate editing a task - await database_sync_to_async(sub_task_update_many)( - who=team_member.user, - task=task, - sub_tasks=[sub_task], - create_sub_tasks=[], - update_sub_tasks=[ - { - "uuid": sub_task.uuid, - "title": sub_task.title, - "done": False, - "_order": 0, - } - ], - ) - assert await expect_change(project_communicator, project) - assert await expect_change(task_communicator, task) - - # Simulate removing a task - await database_sync_to_async(sub_task_update_many)( - who=team_member.user, - task=task, - sub_tasks=[sub_task], - create_sub_tasks=[], - update_sub_tasks=[], - ) - assert await expect_change(project_communicator, project) - assert await expect_change(task_communicator, task) - assert await expect_change(project_communicator, project) - assert await expect_change(task_communicator, task) - - await project_communicator.disconnect() - await task_communicator.disconnect() - - -class TestChatMessage: - """Test consumer behavior for chat messages.""" - - async def test_chat_message_saved_or_deleted( - self, - team_member: TeamMember, - task: Task, - task_communicator: WebsocketCommunicator, - ) -> None: - """Assert event is fired when chat message is saved or deleted.""" - await database_sync_to_async(chat_message_create)( - who=team_member.user, task=task, text="Hello world" - ) - assert await expect_change(task_communicator, task) - # TODO chat messages are not supported right now, - # so no chat_message_delete service exists, and we don't have to delete - # it either - await task_communicator.disconnect() diff --git a/backend/projectify/workspace/test/views/test_project.py b/backend/projectify/workspace/test/views/test_project.py index c9a3d5b8f..5f2bc1e80 100644 --- a/backend/projectify/workspace/test/views/test_project.py +++ b/backend/projectify/workspace/test/views/test_project.py @@ -229,7 +229,7 @@ def test_move_task_up_down( ] # Move task2 up (should swap positions) - with django_assert_num_queries(33): + with django_assert_num_queries(32): response = user_client.post( resource_url, { @@ -245,7 +245,7 @@ def test_move_task_up_down( ] # Move task1 up (should swap back) - with django_assert_num_queries(33): + with django_assert_num_queries(32): response = user_client.post( resource_url, { diff --git a/backend/projectify/workspace/test/views/test_task.py b/backend/projectify/workspace/test/views/test_task.py index 14a658212..ec5366137 100644 --- a/backend/projectify/workspace/test/views/test_task.py +++ b/backend/projectify/workspace/test/views/test_task.py @@ -624,7 +624,8 @@ def test_delete( ) -> None: """Test deleting a task.""" # Gone up from 11 -> 13, since we added filtering annotations - with django_assert_num_queries(14): + # gone down to 12 again Justus 2026-02-23 + with django_assert_num_queries(12): response = rest_user_client.delete(resource_url) assert response.status_code == 204, response.content # Ensure that the task is gone for good @@ -657,7 +658,7 @@ def test_simple( """Test moving a task.""" assert task.section == section # Gone up from 19 -> 23, since we added filtering annotations - with django_assert_num_queries(24): + with django_assert_num_queries(23): response = rest_user_client.post( resource_url, data={"section_uuid": str(other_section.uuid)}, @@ -689,7 +690,7 @@ def test_simple( ) -> None: """Test as an authenticated user.""" # Gone up from 18 -> 22, since we added filtering annotations - with django_assert_num_queries(23): + with django_assert_num_queries(22): response = rest_user_client.post( resource_url, data={"task_uuid": str(other_task.uuid)}, diff --git a/backend/pyproject.toml b/backend/pyproject.toml index c256257fd..7f83ba352 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -19,8 +19,6 @@ exclude = [ [tool.poetry.dependencies] celery = {extras = ["redis"], version = "~5.4"} -channels = {version = "^4.1.0"} -channels-redis = "^4.2" cryptography = "^44.0.1" dj-database-url = "~2.1.0" django = "^6.0.0" @@ -37,7 +35,6 @@ redis = "^5" rules = "^3.3" stripe = "^8" uvicorn = "~0.17.6" -websockets = "~10.3" whitenoise = "~6.2.0" django-ratelimit = "^4.1.0" django-tailwind = {extras = ["reload"], version = "^3.8.0"} @@ -46,9 +43,6 @@ django-markdownify = "^0.9.5" [tool.poetry.group.dev] [tool.poetry.group.dev.dependencies] -# Require the same channels as in the main dependencies, except that we add -# daphne -channels = {version = "^4.1.0", extras = ["daphne"]} python-dotenv = "^1" faker = "^19.13.0" django-extensions = "~3.2.0"