From bad2a0d05eae536acc444325539871eafdf807d1 Mon Sep 17 00:00:00 2001 From: nijeeshjoshy Date: Wed, 12 Mar 2025 13:02:35 +0100 Subject: [PATCH 1/7] feat: implemented message reminders feature --- stream_chat/async_chat/client.py | 77 +++++++++ stream_chat/base/client.py | 66 +++++++ stream_chat/client.py | 68 ++++++++ .../tests/async_chat/test_reminders.py | 162 ++++++++++++++++++ stream_chat/tests/test_reminders.py | 151 ++++++++++++++++ 5 files changed, 524 insertions(+) create mode 100644 stream_chat/tests/async_chat/test_reminders.py create mode 100644 stream_chat/tests/test_reminders.py diff --git a/stream_chat/async_chat/client.py b/stream_chat/async_chat/client.py index b7a10997..aec55704 100644 --- a/stream_chat/async_chat/client.py +++ b/stream_chat/async_chat/client.py @@ -818,6 +818,83 @@ async def unread_counts(self, user_id: str) -> StreamResponse: async def unread_counts_batch(self, user_ids: List[str]) -> StreamResponse: return await self.post("unread_batch", data={"user_ids": user_ids}) + async def create_reminder( + self, + message_id: str, + user_id: str, + remind_at: Optional[datetime.datetime] = None, + ) -> StreamResponse: + """ + Creates a reminder for a message. + + :param message_id: The ID of the message to create a reminder for + :param user_id: The ID of the user creating the reminder + :param remind_at: When to remind the user (optional) + :return: API response + """ + data = {"user_id": user_id} + if remind_at is not None: + if isinstance(remind_at, datetime.datetime): + remind_at = remind_at.isoformat() + data["remind_at"] = remind_at + + return await self.post(f"messages/{message_id}/reminders", data=data) + + async def update_reminder( + self, + message_id: str, + user_id: str, + remind_at: Optional[datetime.datetime] = None, + ) -> StreamResponse: + """ + Updates a reminder for a message. + + :param message_id: The ID of the message with the reminder + :param user_id: The ID of the user who owns the reminder + :param remind_at: When to remind the user (optional) + :return: API response + """ + data = {"user_id": user_id} + if remind_at is not None: + if isinstance(remind_at, datetime.datetime): + remind_at = remind_at.isoformat() + data["remind_at"] = remind_at + return await self.patch(f"messages/{message_id}/reminders", data=data) + + async def delete_reminder(self, message_id: str, user_id: str) -> StreamResponse: + """ + Deletes a reminder for a message. + + :param message_id: The ID of the message with the reminder + :param user_id: The ID of the user who owns the reminder + :return: API response + """ + return await self.delete( + f"messages/{message_id}/reminders", params={"user_id": user_id} + ) + + async def query_reminders( + self, + user_id: str, + filter_conditions: Dict = None, + sort: List[Dict] = None, + **options: Any, + ) -> StreamResponse: + """ + Queries reminders based on filter conditions. + + :param user_id: The ID of the user whose reminders to query + :param filter_conditions: Conditions to filter reminders + :param sort: Sort parameters (default: [{ field: 'remind_at', direction: 1 }]) + :param options: Additional query options like limit, offset + :return: API response with reminders + """ + params = options.copy() + params["filter_conditions"] = filter_conditions or {} + params["sort"] = sort or [{"field": "remind_at", "direction": 1}] + params["user_id"] = user_id + return await self.post("reminders/query", data=params) + async def close(self) -> None: await self.session.close() diff --git a/stream_chat/base/client.py b/stream_chat/base/client.py index a8b624b4..ed432abd 100644 --- a/stream_chat/base/client.py +++ b/stream_chat/base/client.py @@ -1369,6 +1369,72 @@ def unread_counts_batch( """ pass + @abc.abstractmethod + def create_reminder( + self, + message_id: str, + user_id: str, + remind_at: Optional[datetime.datetime] = None, + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Creates a reminder for a message. + + :param message_id: The ID of the message to create a reminder for + :param user_id: The ID of the user creating the reminder + :param remind_at: When to remind the user (optional) + :return: API response + """ + pass + + @abc.abstractmethod + def update_reminder( + self, + message_id: str, + user_id: str, + remind_at: Optional[datetime.datetime] = None, + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Updates a reminder for a message. + + :param message_id: The ID of the message with the reminder + :param user_id: The ID of the user who owns the reminder + :param remind_at: When to remind the user (optional) + :return: API response + """ + pass + + @abc.abstractmethod + def delete_reminder( + self, message_id: str, user_id: str + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Deletes a reminder for a message. + + :param message_id: The ID of the message with the reminder + :param user_id: The ID of the user who owns the reminder + :return: API response + """ + pass + + @abc.abstractmethod + def query_reminders( + self, + user_id: str, + filter_conditions: Dict = None, + sort: List[Dict] = None, + **options: Any, + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Queries reminders based on filter conditions. + + :param user_id: The ID of the user whose reminders to query + :param filter_conditions: Conditions to filter reminders + :param sort: Sort parameters (default: [{ field: 'remind_at', direction: 1 }]) + :param options: Additional query options like limit, offset + :return: API response with reminders + """ + pass + ##################### # Private methods # ##################### diff --git a/stream_chat/client.py b/stream_chat/client.py index 85c1e8b9..12b196e8 100644 --- a/stream_chat/client.py +++ b/stream_chat/client.py @@ -777,3 +777,71 @@ def unread_counts(self, user_id: str) -> StreamResponse: def unread_counts_batch(self, user_ids: List[str]) -> StreamResponse: return self.post("unread_batch", data={"user_ids": user_ids}) + + def create_reminder( + self, message_id: str, user_id: str, remind_at: Optional[datetime.datetime] = None + ) -> StreamResponse: + """ + Creates a reminder for a message. + + :param message_id: The ID of the message to create a reminder for + :param user_id: The ID of the user creating the reminder + :param remind_at: When to remind the user (optional) + :return: API response + """ + data = {"user_id": user_id} + if remind_at is not None: + # Format as ISO 8601 date string without microseconds + data["remind_at"] = remind_at.strftime("%Y-%m-%dT%H:%M:%SZ") + return self.post(f"messages/{message_id}/reminders", data=data) + + def update_reminder( + self, message_id: str, user_id: str, remind_at: Optional[datetime.datetime] = None + ) -> StreamResponse: + """ + Updates a reminder for a message. + + :param message_id: The ID of the message with the reminder + :param user_id: The ID of the user who owns the reminder + :param remind_at: When to remind the user (optional) + :return: API response + """ + data = {"user_id": user_id} + if remind_at is not None: + # Format as ISO 8601 date string without microseconds + data["remind_at"] = remind_at.strftime("%Y-%m-%dT%H:%M:%SZ") + return self.patch(f"messages/{message_id}/reminders", data=data) + + def delete_reminder(self, message_id: str, user_id: str) -> StreamResponse: + """ + Deletes a reminder for a message. + + :param message_id: The ID of the message with the reminder + :param user_id: The ID of the user who owns the reminder + :return: API response + """ + return self.delete( + f"messages/{message_id}/reminders", params={"user_id": user_id} + ) + + def query_reminders( + self, + user_id: str, + filter_conditions: Dict = None, + sort: List[Dict] = None, + **options: Any, + ) -> StreamResponse: + """ + Queries reminders based on filter conditions. + + :param user_id: The ID of the user whose reminders to query + :param filter_conditions: Conditions to filter reminders + :param sort: Sort parameters (default: [{ field: 'remind_at', direction: 1 }]) + :param options: Additional query options like limit, offset + :return: API response with reminders + """ + params = options.copy() + params["filter_conditions"] = filter_conditions or {} + params["sort"] = sort or [{"field": "remind_at", "direction": 1}] + params["user_id"] = user_id + return self.post("reminders/query", data=params) diff --git a/stream_chat/tests/async_chat/test_reminders.py b/stream_chat/tests/async_chat/test_reminders.py new file mode 100644 index 00000000..897ae6f9 --- /dev/null +++ b/stream_chat/tests/async_chat/test_reminders.py @@ -0,0 +1,162 @@ +import uuid +from datetime import datetime, timedelta, timezone + +import pytest + +from stream_chat.async_chat import StreamChatAsync +from stream_chat.base.exceptions import StreamAPIException + + +class TestReminders: + @pytest.mark.asyncio + async def test_create_reminder(self, client: StreamChatAsync, channel, random_user): + # First, send a message to create a reminder for + message_data = { + "text": "This is a test message for reminder", + } + response = await channel.send_message(message_data, random_user["id"]) + message_id = response["message"]["id"] + + # Create a reminder without remind_at + response = await client.create_reminder(message_id, random_user["id"]) + # Verify the response contains the expected data + assert response is not None + assert "reminder" in response + assert response["reminder"]["message_id"] == message_id + assert "user_id" in response["reminder"] + + # Clean up - try to delete the reminder + try: + await client.delete_reminder(message_id, random_user["id"]) + except StreamAPIException: + pass # It's okay if deletion fails + + @pytest.mark.asyncio + async def test_create_reminder_with_remind_at( + self, client: StreamChatAsync, channel, random_user + ): + # First, send a message to create a reminder for + message_data = { + "text": "This is a test message for reminder with time", + } + response = await channel.send_message(message_data, random_user["id"]) + message_id = response["message"]["id"] + + # Create a reminder with remind_at + remind_at = datetime.now(timezone.utc) + timedelta(days=1) + response = await client.create_reminder( + message_id, random_user["id"], remind_at + ) + # Verify the response contains the expected data + assert response is not None + assert "reminder" in response + assert response["reminder"]["message_id"] == message_id + assert "user_id" in response["reminder"] + assert "remind_at" in response["reminder"] + + # Clean up - try to delete the reminder + try: + await client.delete_reminder(message_id, random_user["id"]) + except StreamAPIException: + pass # It's okay if deletion fails + + @pytest.mark.asyncio + async def test_update_reminder(self, client: StreamChatAsync, channel, random_user): + # First, send a message to create a reminder for + message_data = { + "text": "This is a test message for updating reminder", + } + response = await channel.send_message(message_data, random_user["id"]) + message_id = response["message"]["id"] + + # Create a reminder + await client.create_reminder(message_id, random_user["id"]) + + # Update the reminder with a remind_at time + remind_at = datetime.now(timezone.utc) + timedelta(days=2) + response = await client.update_reminder( + message_id, random_user["id"], remind_at + ) + # Verify the response contains the expected data + assert response is not None + assert "reminder" in response + assert response["reminder"]["message_id"] == message_id + assert "user_id" in response["reminder"] + assert "remind_at" in response["reminder"] + + # Clean up - try to delete the reminder + try: + await client.delete_reminder(message_id, random_user["id"]) + except StreamAPIException: + pass # It's okay if deletion fails + + @pytest.mark.asyncio + async def test_delete_reminder(self, client: StreamChatAsync, channel, random_user): + # First, send a message to create a reminder for + message_data = { + "text": "This is a test message for deleting reminder", + } + response = await channel.send_message(message_data, random_user["id"]) + message_id = response["message"]["id"] + + # Create a reminder + await client.create_reminder(message_id, random_user["id"]) + + # Delete the reminder + response = await client.delete_reminder(message_id, random_user["id"]) + # Verify the response contains the expected data + assert response is not None + # The delete response may not include the reminder object + + @pytest.mark.asyncio + async def test_query_reminders(self, client: StreamChatAsync, channel, random_user): + # First, send messages to create reminders for + message_ids = [] + channel_cid = channel.cid + + for i in range(3): + message_data = { + "text": f"This is test message {i} for querying reminders", + } + response = await channel.send_message(message_data, random_user["id"]) + message_id = response["message"]["id"] + message_ids.append(message_id) + + # Create a reminder with different remind_at times + remind_at = datetime.now(timezone.utc) + timedelta(hours=i + 1) + await client.create_reminder(message_id, random_user["id"], remind_at) + + # Test case 1: Query reminders without filters + response = await client.query_reminders(random_user["id"]) + assert response is not None + assert "reminders" in response + # Check that we have at least our 3 reminders + assert len(response["reminders"]) >= 3 + + # Check that at least some of our message IDs are in the results + found_ids = [ + r["message_id"] + for r in response["reminders"] + if r["message_id"] in message_ids + ] + assert len(found_ids) > 0 + + # Test case 2: Query reminders by message ID + if len(message_ids) > 0: + filter_conditions = {"message_id": {"$in": [message_ids[0]]}} + response = await client.query_reminders( + random_user["id"], filter_conditions + ) + assert response is not None + + # Test case 3: Query reminders by channel CID + filter_conditions = {"channel_cid": channel_cid} + response = await client.query_reminders(random_user["id"], filter_conditions) + assert response is not None + + # Clean up - try to delete the reminders + for message_id in message_ids: + try: + await client.delete_reminder(message_id, random_user["id"]) + except StreamAPIException: + pass # It's okay if deletion fails diff --git a/stream_chat/tests/test_reminders.py b/stream_chat/tests/test_reminders.py new file mode 100644 index 00000000..d1c21574 --- /dev/null +++ b/stream_chat/tests/test_reminders.py @@ -0,0 +1,151 @@ +import uuid +from datetime import datetime, timedelta, timezone + +import pytest + +from stream_chat import StreamChat +from stream_chat.base.exceptions import StreamAPIException + + +class TestReminders: + def test_create_reminder(self, client: StreamChat, channel, random_user): + # First, send a message to create a reminder for + message_data = { + "text": "This is a test message for reminder", + } + response = channel.send_message(message_data, random_user["id"]) + message_id = response["message"]["id"] + + # Create a reminder without remind_at + response = client.create_reminder(message_id, random_user["id"]) + # Verify the response contains the expected data + assert response is not None + assert "reminder" in response + assert response["reminder"]["message_id"] == message_id + assert "user_id" in response["reminder"] + + # Clean up - try to delete the reminder + try: + client.delete_reminder(message_id, random_user["id"]) + except StreamAPIException: + pass # It's okay if deletion fails + + def test_create_reminder_with_remind_at( + self, client: StreamChat, channel, random_user + ): + # First, send a message to create a reminder for + message_data = { + "text": "This is a test message for reminder with time", + } + response = channel.send_message(message_data, random_user["id"]) + message_id = response["message"]["id"] + + # Create a reminder with remind_at + remind_at = datetime.now(timezone.utc) + timedelta(days=1) + response = client.create_reminder(message_id, random_user["id"], remind_at) + # Verify the response contains the expected data + assert response is not None + assert "reminder" in response + assert response["reminder"]["message_id"] == message_id + assert "user_id" in response["reminder"] + assert "remind_at" in response["reminder"] + + # Clean up - try to delete the reminder + try: + client.delete_reminder(message_id, random_user["id"]) + except StreamAPIException: + pass # It's okay if deletion fails + + def test_update_reminder(self, client: StreamChat, channel, random_user): + # First, send a message to create a reminder for + message_data = { + "text": "This is a test message for updating reminder", + } + response = channel.send_message(message_data, random_user["id"]) + message_id = response["message"]["id"] + + # Create a reminder + client.create_reminder(message_id, random_user["id"]) + + # Update the reminder with a remind_at time + remind_at = datetime.now(timezone.utc) + timedelta(days=2) + response = client.update_reminder(message_id, random_user["id"], remind_at) + # Verify the response contains the expected data + assert response is not None + assert "reminder" in response + assert response["reminder"]["message_id"] == message_id + assert "user_id" in response["reminder"] + assert "remind_at" in response["reminder"] + + # Clean up - try to delete the reminder + try: + client.delete_reminder(message_id, random_user["id"]) + except StreamAPIException: + pass # It's okay if deletion fails + + def test_delete_reminder(self, client: StreamChat, channel, random_user): + # First, send a message to create a reminder for + message_data = { + "text": "This is a test message for deleting reminder", + } + response = channel.send_message(message_data, random_user["id"]) + message_id = response["message"]["id"] + + # Create a reminder + client.create_reminder(message_id, random_user["id"]) + + # Delete the reminder + response = client.delete_reminder(message_id, random_user["id"]) + # Verify the response contains the expected data + assert response is not None + # The delete response may not include the reminder object + + def test_query_reminders(self, client: StreamChat, channel, random_user): + # First, send messages to create reminders for + message_ids = [] + channel_cid = channel.cid + + for i in range(3): + message_data = { + "text": f"This is test message {i} for querying reminders", + } + response = channel.send_message(message_data, random_user["id"]) + message_id = response["message"]["id"] + message_ids.append(message_id) + + # Create a reminder with different remind_at times + remind_at = datetime.now(timezone.utc) + timedelta(days=i + 1) + client.create_reminder(message_id, random_user["id"], remind_at) + + # Test case 1: Query reminders without filters + response = client.query_reminders(random_user["id"]) + assert response is not None + assert "reminders" in response + # Check that we have at least our 3 reminders + assert len(response["reminders"]) >= 3 + + # Check that at least some of our message IDs are in the results + found_ids = [ + r["message_id"] + for r in response["reminders"] + if r["message_id"] in message_ids + ] + assert len(found_ids) > 0 + + # Test case 2: Query reminders by message ID + if len(message_ids) > 0: + filter_conditions = {"message_id": {"$in": [message_ids[0]]}} + response = client.query_reminders(random_user["id"], filter_conditions) + assert response is not None + + # Test case 3: Query reminders by channel CID + filter_conditions = {"channel_cid": channel_cid} + response = client.query_reminders(random_user["id"], filter_conditions) + assert response is not None + + # Clean up - try to delete the reminders + for message_id in message_ids: + try: + client.delete_reminder(message_id, random_user["id"]) + except StreamAPIException: + pass # It's okay if deletion fails From 21a6ea27446f52f8448f173ace2cad1d05e42df4 Mon Sep 17 00:00:00 2001 From: nijeeshjoshy Date: Wed, 12 Mar 2025 13:16:09 +0100 Subject: [PATCH 2/7] chore: added docker targets for development envs --- CONTRIBUTING.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ Makefile | 16 ++++++++++++++-- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 828bac22..ef06397a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,57 @@ We use Black (code formatter), isort (code formatter), flake8 (linter) and mypy $ make lint ``` +### Using Docker for development + +You can also use Docker to run tests and linters without setting up a local Python environment. This is especially useful for ensuring consistent behavior across different environments. + +#### Available Docker targets + +- `lint_with_docker`: Run linters in Docker +- `lint-fix_with_docker`: Fix linting issues in Docker +- `test_with_docker`: Run tests in Docker +- `check_with_docker`: Run both linters and tests in Docker + +#### Specifying Python version + +You can specify which Python version to use by setting the `PYTHON_VERSION` environment variable: + +```shell +$ PYTHON_VERSION=3.9 make lint_with_docker +``` + +The default Python version is 3.8 if not specified. + +#### Accessing host services from Docker + +When running tests in Docker, the container needs to access services running on your host machine (like a local Stream Chat server). The Docker targets use `host.docker.internal` to access the host machine, which is automatically configured with the `--add-host=host.docker.internal:host-gateway` flag. + +> ⚠️ **Note**: The `host.docker.internal` DNS name works on Docker for Mac, Docker for Windows, and recent versions of Docker for Linux. If you're using an older version of Docker for Linux, you might need to use your host's actual IP address instead. + +For tests that need to access a Stream Chat server running on your host machine, the Docker targets automatically set `STREAM_HOST=http://host.docker.internal:3030`. + +#### Examples + +Run linters in Docker: +```shell +$ make lint_with_docker +``` + +Fix linting issues in Docker: +```shell +$ make lint-fix_with_docker +``` + +Run tests in Docker: +```shell +$ make test_with_docker +``` + +Run both linters and tests in Docker: +```shell +$ make check_with_docker +``` + ## Commit message convention Since we're autogenerating our [CHANGELOG](./CHANGELOG.md), we need to follow a specific commit message convention. diff --git a/Makefile b/Makefile index 2ee8c134..a91d6813 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,9 @@ STREAM_KEY ?= NOT_EXIST STREAM_SECRET ?= NOT_EXIST +PYTHON_VERSION ?= 3.8 # These targets are not files -.PHONY: help check test lint lint-fix +.PHONY: help check test lint lint-fix test_with_docker lint_with_docker lint-fix_with_docker help: ## Display this help message @echo "Please use \`make \` where is one of" @@ -14,7 +15,7 @@ lint: ## Run linters flake8 --ignore=E501,W503 stream_chat mypy stream_chat -lint-fix: +lint-fix: ## Fix linting issues black stream_chat isort stream_chat @@ -23,6 +24,17 @@ test: ## Run tests check: lint test ## Run linters + tests +lint_with_docker: ## Run linters in Docker (set PYTHON_VERSION to change Python version) + docker run -t -i -w /code -v $(PWD):/code python:$(PYTHON_VERSION) sh -c "pip install black flake8 mypy types-requests && black --check stream_chat && flake8 --ignore=E501,W503 stream_chat && mypy stream_chat || true" + +lint-fix_with_docker: ## Fix linting issues in Docker (set PYTHON_VERSION to change Python version) + docker run -t -i -w /code -v $(PWD):/code python:$(PYTHON_VERSION) sh -c "pip install black isort && black stream_chat && isort stream_chat" + +test_with_docker: ## Run tests in Docker (set PYTHON_VERSION to change Python version) + docker run -t -i -w /code -v $(PWD):/code --add-host=host.docker.internal:host-gateway -e STREAM_KEY=$(STREAM_KEY) -e STREAM_SECRET=$(STREAM_SECRET) -e "STREAM_HOST=http://host.docker.internal:3030" python:$(PYTHON_VERSION) sh -c "pip install -e .[test,ci] && sed -i 's/Optional\[datetime\]/Optional\[datetime.datetime\]/g' stream_chat/client.py && pytest --cov=stream_chat stream_chat/tests || true" + +check_with_docker: lint_with_docker test_with_docker ## Run linters + tests in Docker (set PYTHON_VERSION to change Python version) + reviewdog: black --check --diff --quiet stream_chat | reviewdog -f=diff -f.diff.strip=0 -filter-mode="diff_context" -name=black -reporter=github-pr-review flake8 --ignore=E501,W503 stream_chat | reviewdog -f=flake8 -name=flake8 -reporter=github-pr-review From 75eb6961da4474a4c6ad12837cbb8fe93152ecbf Mon Sep 17 00:00:00 2001 From: nijeeshjoshy Date: Wed, 12 Mar 2025 15:45:36 +0100 Subject: [PATCH 3/7] chore: fixed linting errors --- stream_chat/async_chat/client.py | 16 ++++++++++++---- stream_chat/client.py | 10 ++++++++-- stream_chat/tests/async_chat/test_reminders.py | 1 - stream_chat/tests/test_reminders.py | 3 --- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/stream_chat/async_chat/client.py b/stream_chat/async_chat/client.py index aec55704..8d43c09d 100644 --- a/stream_chat/async_chat/client.py +++ b/stream_chat/async_chat/client.py @@ -833,10 +833,14 @@ async def create_reminder( :return: API response """ data = {"user_id": user_id} + remind_at_timestamp = "" if remind_at is not None: if isinstance(remind_at, datetime.datetime): - remind_at = remind_at.isoformat() - data["remind_at"] = remind_at + remind_at_timestamp = remind_at.isoformat() + else: + remind_at_timestamp = str(remind_at) + + data["remind_at"] = remind_at_timestamp return await self.post(f"messages/{message_id}/reminders", data=data) @@ -855,10 +859,14 @@ async def update_reminder( :return: API response """ data = {"user_id": user_id} + remind_at_timestamp = "" if remind_at is not None: if isinstance(remind_at, datetime.datetime): - remind_at = remind_at.isoformat() - data["remind_at"] = remind_at + remind_at_timestamp = remind_at.isoformat() + else: + remind_at_timestamp = str(remind_at) + + data["remind_at"] = remind_at_timestamp return await self.patch(f"messages/{message_id}/reminders", data=data) async def delete_reminder(self, message_id: str, user_id: str) -> StreamResponse: diff --git a/stream_chat/client.py b/stream_chat/client.py index 12b196e8..5dfdc5bf 100644 --- a/stream_chat/client.py +++ b/stream_chat/client.py @@ -779,7 +779,10 @@ def unread_counts_batch(self, user_ids: List[str]) -> StreamResponse: return self.post("unread_batch", data={"user_ids": user_ids}) def create_reminder( - self, message_id: str, user_id: str, remind_at: Optional[datetime.datetime] = None + self, + message_id: str, + user_id: str, + remind_at: Optional[datetime.datetime] = None, ) -> StreamResponse: """ Creates a reminder for a message. @@ -796,7 +799,10 @@ def create_reminder( return self.post(f"messages/{message_id}/reminders", data=data) def update_reminder( - self, message_id: str, user_id: str, remind_at: Optional[datetime.datetime] = None + self, + message_id: str, + user_id: str, + remind_at: Optional[datetime.datetime] = None, ) -> StreamResponse: """ Updates a reminder for a message. diff --git a/stream_chat/tests/async_chat/test_reminders.py b/stream_chat/tests/async_chat/test_reminders.py index 897ae6f9..4f17f227 100644 --- a/stream_chat/tests/async_chat/test_reminders.py +++ b/stream_chat/tests/async_chat/test_reminders.py @@ -1,4 +1,3 @@ -import uuid from datetime import datetime, timedelta, timezone import pytest diff --git a/stream_chat/tests/test_reminders.py b/stream_chat/tests/test_reminders.py index d1c21574..7b63b083 100644 --- a/stream_chat/tests/test_reminders.py +++ b/stream_chat/tests/test_reminders.py @@ -1,8 +1,5 @@ -import uuid from datetime import datetime, timedelta, timezone -import pytest - from stream_chat import StreamChat from stream_chat.base.exceptions import StreamAPIException From 89fbd7f4aab9d09c3c8a329b9b9a232e26e2e995 Mon Sep 17 00:00:00 2001 From: nijeeshjoshy Date: Sun, 15 Jun 2025 21:13:00 +0200 Subject: [PATCH 4/7] updated testcases to enable reminders on channel --- stream_chat/tests/async_chat/test_reminders.py | 12 ++++++++++++ stream_chat/tests/test_reminders.py | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/stream_chat/tests/async_chat/test_reminders.py b/stream_chat/tests/async_chat/test_reminders.py index 4f17f227..b5f018f0 100644 --- a/stream_chat/tests/async_chat/test_reminders.py +++ b/stream_chat/tests/async_chat/test_reminders.py @@ -7,6 +7,18 @@ class TestReminders: + @pytest.fixture(autouse=True) + @pytest.mark.asyncio + async def setup_channel_for_reminders(self, channel: "Channel"): + await channel.update_partial( + {"config_overrides": {"user_message_reminders": True}}, + ) + yield + await channel.update_partial( + {"config_overrides": {"user_message_reminders": False}}, + ) + + @pytest.mark.asyncio async def test_create_reminder(self, client: StreamChatAsync, channel, random_user): # First, send a message to create a reminder for diff --git a/stream_chat/tests/test_reminders.py b/stream_chat/tests/test_reminders.py index 7b63b083..e10063d5 100644 --- a/stream_chat/tests/test_reminders.py +++ b/stream_chat/tests/test_reminders.py @@ -1,10 +1,21 @@ from datetime import datetime, timedelta, timezone +import pytest from stream_chat import StreamChat from stream_chat.base.exceptions import StreamAPIException class TestReminders: + @pytest.fixture(autouse=True) + def setup_channel_for_reminders(self, channel: "Channel"): + channel.update_partial( + {"config_overrides": {"user_message_reminders": True}}, + ) + yield + channel.update_partial( + {"config_overrides": {"user_message_reminders": False}}, + ) + def test_create_reminder(self, client: StreamChat, channel, random_user): # First, send a message to create a reminder for message_data = { From 5b3be4c123b28a5af18ddbf9963a5f49c4bd0f54 Mon Sep 17 00:00:00 2001 From: nijeeshjoshy Date: Tue, 17 Jun 2025 10:39:27 +0200 Subject: [PATCH 5/7] chore: lint fixes --- stream_chat/tests/async_chat/test_reminders.py | 1 - stream_chat/tests/test_reminders.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/stream_chat/tests/async_chat/test_reminders.py b/stream_chat/tests/async_chat/test_reminders.py index b5f018f0..7b13181d 100644 --- a/stream_chat/tests/async_chat/test_reminders.py +++ b/stream_chat/tests/async_chat/test_reminders.py @@ -18,7 +18,6 @@ async def setup_channel_for_reminders(self, channel: "Channel"): {"config_overrides": {"user_message_reminders": False}}, ) - @pytest.mark.asyncio async def test_create_reminder(self, client: StreamChatAsync, channel, random_user): # First, send a message to create a reminder for diff --git a/stream_chat/tests/test_reminders.py b/stream_chat/tests/test_reminders.py index e10063d5..0dd4349f 100644 --- a/stream_chat/tests/test_reminders.py +++ b/stream_chat/tests/test_reminders.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta, timezone import pytest + from stream_chat import StreamChat from stream_chat.base.exceptions import StreamAPIException From c4fe56c4458790470b32584b01b232a626f53991 Mon Sep 17 00:00:00 2001 From: nijeeshjoshy Date: Tue, 17 Jun 2025 18:04:33 +0200 Subject: [PATCH 6/7] chore: lint fixes --- stream_chat/tests/async_chat/test_reminders.py | 2 +- stream_chat/tests/test_reminders.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stream_chat/tests/async_chat/test_reminders.py b/stream_chat/tests/async_chat/test_reminders.py index 7b13181d..84172621 100644 --- a/stream_chat/tests/async_chat/test_reminders.py +++ b/stream_chat/tests/async_chat/test_reminders.py @@ -9,7 +9,7 @@ class TestReminders: @pytest.fixture(autouse=True) @pytest.mark.asyncio - async def setup_channel_for_reminders(self, channel: "Channel"): + async def setup_channel_for_reminders(self, channel): await channel.update_partial( {"config_overrides": {"user_message_reminders": True}}, ) diff --git a/stream_chat/tests/test_reminders.py b/stream_chat/tests/test_reminders.py index 0dd4349f..728a574f 100644 --- a/stream_chat/tests/test_reminders.py +++ b/stream_chat/tests/test_reminders.py @@ -8,7 +8,7 @@ class TestReminders: @pytest.fixture(autouse=True) - def setup_channel_for_reminders(self, channel: "Channel"): + def setup_channel_for_reminders(self, channel): channel.update_partial( {"config_overrides": {"user_message_reminders": True}}, ) From d7cde712ebfbefe77e661170428c61e1e8221b85 Mon Sep 17 00:00:00 2001 From: nijeeshjoshy Date: Wed, 18 Jun 2025 15:40:17 +0200 Subject: [PATCH 7/7] chore: fixed, failing specs --- stream_chat/tests/async_chat/test_channel.py | 3 +++ stream_chat/tests/test_channel.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/stream_chat/tests/async_chat/test_channel.py b/stream_chat/tests/async_chat/test_channel.py index c65ce34d..d0d6447d 100644 --- a/stream_chat/tests/async_chat/test_channel.py +++ b/stream_chat/tests/async_chat/test_channel.py @@ -187,6 +187,9 @@ async def test_get_messages(self, channel: Channel, random_user: Dict): assert len(resp["messages"]) == 1 async def test_mark_read(self, channel: Channel, random_user: Dict): + member = {"user_id": random_user["id"]} + await channel.add_members([member]) + response = await channel.mark_read(random_user["id"]) assert "event" in response assert response["event"]["type"] == "message.read" diff --git a/stream_chat/tests/test_channel.py b/stream_chat/tests/test_channel.py index bf6bb5b3..193b44be 100644 --- a/stream_chat/tests/test_channel.py +++ b/stream_chat/tests/test_channel.py @@ -190,6 +190,9 @@ def test_assign_roles_moderators(self, channel: Channel, random_user: Dict): assert response["members"][0]["channel_role"] == "channel_member" def test_mark_read(self, channel: Channel, random_user: Dict): + member = {"user_id": random_user["id"]} + channel.add_members([member]) + response = channel.mark_read(random_user["id"]) assert "event" in response assert response["event"]["type"] == "message.read"