Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 14 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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 <target>\` where <target> is one of"
Expand All @@ -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

Expand All @@ -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
Expand Down
85 changes: 85 additions & 0 deletions stream_chat/async_chat/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,91 @@ async def query_drafts(

return await self.post("drafts/query", data=data)

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}
remind_at_timestamp = ""
if remind_at is not None:
if isinstance(remind_at, datetime.datetime):
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)

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}
remind_at_timestamp = ""
if remind_at is not None:
if isinstance(remind_at, datetime.datetime):
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:
"""
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()

Expand Down
66 changes: 66 additions & 0 deletions stream_chat/base/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1439,6 +1439,72 @@ def query_drafts(
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
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 #
#####################
Expand Down
74 changes: 74 additions & 0 deletions stream_chat/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,3 +824,77 @@ def query_drafts(
if options is not None:
data.update(cast(dict, options))
return self.post("drafts/query", data=data)

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)
3 changes: 3 additions & 0 deletions stream_chat/tests/async_chat/test_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading