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
5 changes: 5 additions & 0 deletions .cursor/worktrees.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"setup-worktree": [
"npm install"
]
}
Comment on lines +1 to +5
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for package.json and Node.js indicators
fd -t f "package.json" --max-depth 3

# Check for Python project indicators
fd -t f -e "py|toml|txt" "(setup|pyproject|requirements)" --max-depth 2

# List key files in root to understand project type
git ls-files | head -20

Repository: GetStream/stream-py

Length of output: 469


🏁 Script executed:

#!/bin/bash
# Check for npm references in the repository
rg -i "npm\s+(install|run)" --max-count=10

# Check workflows for any Node.js setup
rg -i "node|npm" .github/workflows/ --max-count=10

# Verify no package.json exists anywhere
find . -name "package.json" 2>/dev/null

# Check pre-commit config for Node.js hooks
cat .pre-commit-config.yaml 2>/dev/null | head -30

Repository: GetStream/stream-py

Length of output: 453


Replace npm install with appropriate Python setup command.

This is a Python project with no Node.js dependencies. The worktree setup should use uv install or pip install -e . instead. Currently, npm install will fail since there's no package.json in the repository, breaking the Cursor IDE worktree setup for this project.

🤖 Prompt for AI Agents
In @.cursor/worktrees.json around lines 1 - 5, The worktree setup currently runs
"npm install" which is incorrect for this Python project; update the
"setup-worktree" entry in .cursor/worktrees.json to use the appropriate Python
install command (e.g., replace the "npm install" array element with "uv install"
or "pip install -e .") so the Cursor IDE worktree setup succeeds; ensure the
"setup-worktree" array only contains valid shell commands for Python environment
setup.

31 changes: 31 additions & 0 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,37 @@ jobs:
- name: Run Ruff formatter
run: uv run ruff format --check .

typecheck:
name: Type Check (ty)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install dependencies
uses: ./.github/actions/python-uv-setup
- name: Run ty type checker
run: |
uvx ty check getstream/ \
--exclude "getstream/models/" \
--exclude "getstream/video/rtc/pb/" \
--exclude "**/rest_client.py" \
--exclude "**/async_rest_client.py" \
--exclude "getstream/chat/channel.py" \
--exclude "getstream/chat/async_channel.py" \
--exclude "getstream/chat/client.py" \
--exclude "getstream/chat/async_client.py" \
--exclude "getstream/common/client.py" \
--exclude "getstream/common/async_client.py" \
--exclude "getstream/moderation/client.py" \
--exclude "getstream/moderation/async_client.py" \
--exclude "getstream/video/client.py" \
--exclude "getstream/video/async_client.py" \
--exclude "getstream/video/call.py" \
--exclude "getstream/video/async_call.py" \
--exclude "getstream/feeds/client.py" \
--exclude "getstream/feeds/feeds.py" \
--exclude "getstream/stream.py"

test:
name: Test "${{ inputs.marker }}"
environment:
Expand Down
15 changes: 11 additions & 4 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ pre-commit install

### Check

Shortcut to ruff, mypy and non integration tests:
Shortcut to ruff, ty (type checker) and non integration tests:

```
uv run python dev.py check
uv run python dev.py
```
Comment on lines +32 to 36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor grammar and markdown lint issues.

  1. "non integration tests" should be hyphenated as "non-integration tests"
  2. The code block on line 34 is missing a language specifier
Suggested fix
-Shortcut to ruff, ty (type checker) and non integration tests:
+Shortcut to ruff, ty (type checker) and non-integration tests:

-```
+```bash
 uv run python dev.py
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
Shortcut to ruff, ty (type checker) and non-integration tests:

🧰 Tools
🪛 LanguageTool

[grammar] ~32-~32: Use a hyphen to join words.
Context: ...rtcut to ruff, ty (type checker) and non integration tests: ``` uv run python de...

(QB_NEW_EN_HYPHEN)

🪛 markdownlint-cli2 (0.18.1)

34-34: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In @DEVELOPMENT.md around lines 32 - 36, Update the text "non integration tests"
to use the hyphenated form "non-integration tests" and add a language specifier
to the fenced code block containing `uv run python dev.py` (e.g., change ``` to
```bash) so the snippet is correctly marked as bash.


### Formatting
Expand All @@ -41,10 +41,17 @@ uv run python dev.py check
uv run ruff check --fix
```

### Mypy type checks
### Type checking (ty)

Type checking is run via the `ty` type checker, excluding generated code:

```
uv run python dev.py ty
```

Or manually (note: requires exclude flags for generated code - see dev.py for the full list):
```
uv run mypy --install-types --non-interactive --exclude 'getstream/models/.*' .
uvx ty check getstream/ --exclude "getstream/models/" --exclude "getstream/video/rtc/pb/" ...
```
Comment on lines +44 to 55
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language specifiers to code blocks.

Static analysis indicates the code blocks are missing language specifiers for proper syntax highlighting.

Suggested fix
 ### Type checking (ty)

 Type checking is run via the `ty` type checker, excluding generated code:

-```
+```bash
 uv run python dev.py ty

Or manually (note: requires exclude flags for generated code - see dev.py for the full list):
- +bash
uvx ty check getstream/ --exclude "getstream/models/" --exclude "getstream/video/rtc/pb/" ...

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

48-48: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


53-53: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In @DEVELOPMENT.md around lines 44 - 55, The fenced code blocks that show CLI
commands (e.g., the block containing "uv run python dev.py ty" and the block
containing "uvx ty check getstream/ --exclude ...") lack language specifiers;
update both opening fences to use ```bash so the blocks become ```bash and
enable proper syntax highlighting for those command examples.


## Release
Expand Down
80 changes: 67 additions & 13 deletions dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

def run(
command: str, env: Optional[dict] = None, check: bool = True
) -> subprocess.CompletedProcess:
) -> subprocess.CompletedProcess[str]:
"""Run a shell command with automatic argument parsing."""
click.echo(f"Running: {command}")

Expand All @@ -24,8 +24,8 @@ def run(
if env:
full_env.update(env)

cmd_list = shlex.split(command)
try:
cmd_list = shlex.split(command)
result = subprocess.run(
cmd_list, check=check, capture_output=False, env=full_env, text=True
)
Expand All @@ -34,15 +34,18 @@ def run(
if check:
click.echo(f"Command failed with exit code {e.returncode}", err=True)
sys.exit(e.returncode)
return e
# Return a CompletedProcess with the error info when check=False
return subprocess.CompletedProcess(
cmd_list, e.returncode, stdout=None, stderr=None
)


@click.group(invoke_without_command=True)
@click.pass_context
def cli(ctx):
"""Development CLI tool for getstream SDK."""
"""Development CLI tool for getstream SDK. Runs 'check' by default."""
if ctx.invoked_subcommand is None:
click.echo(ctx.get_help())
ctx.invoke(check)


@cli.command()
Expand Down Expand Up @@ -75,26 +78,63 @@ def lint():
run("uv run ruff format --check .")


# Generated code patterns to exclude from type checking
TY_EXCLUDES = [
"getstream/models/",
"getstream/video/rtc/pb/",
"**/rest_client.py",
"**/async_rest_client.py",
"getstream/chat/channel.py",
"getstream/chat/async_channel.py",
"getstream/chat/client.py",
"getstream/chat/async_client.py",
"getstream/common/client.py",
"getstream/common/async_client.py",
"getstream/moderation/client.py",
"getstream/moderation/async_client.py",
"getstream/video/client.py",
"getstream/video/async_client.py",
"getstream/video/call.py",
"getstream/video/async_call.py",
"getstream/feeds/client.py",
"getstream/feeds/feeds.py",
"getstream/stream.py",
]


def _run_typecheck():
"""Internal function to run ty type checks."""
click.echo("Running ty type checker on getstream...")
excludes = " ".join(f'--exclude "{e}"' for e in TY_EXCLUDES)
run(f"uvx ty check getstream/ {excludes}")


@cli.command()
def mypy():
"""Run mypy type checks on getstream package."""
click.echo("Running mypy on getstream...")
run("uv run mypy --install-types --non-interactive getstream")
def typecheck():
"""Run ty type checks on getstream package (excludes generated code)."""
_run_typecheck()


@cli.command("ty")
def ty_alias():
"""Run ty type checks (alias for 'typecheck')."""
_run_typecheck()


@cli.command()
def check():
"""Run full check: ruff, mypy, and unit tests."""
"""Run full check: ruff, ty, and unit tests."""
click.echo("Running full development check...")

# Run ruff
click.echo("\n=== 1. Ruff Linting ===")
run("uv run ruff check .")
run("uv run ruff format --check .")

# Run mypy on main package
click.echo("\n=== 2. MyPy Type Checking ===")
run("uv run mypy --install-types --non-interactive getstream")
# Run ty type checker on main package (excludes generated code)
click.echo("\n=== 2. Type Checking (ty) ===")
excludes = " ".join(f'--exclude "{e}"' for e in TY_EXCLUDES)
run(f"uvx ty check getstream/ {excludes}")

# Run unit tests
click.echo("\n=== 3. Unit Tests ===")
Expand All @@ -103,5 +143,19 @@ def check():
click.echo("\n✅ All checks passed!")


@cli.command()
def regen():
"""Regenerate all generated code (OpenAPI + WebRTC protobuf)."""
click.echo("Regenerating all generated code...")

click.echo("\n=== 1. OpenAPI Code Generation ===")
run("./generate.sh")

click.echo("\n=== 2. WebRTC Protobuf Generation ===")
run("./generate_webrtc.sh")

click.echo("\n✅ Code regeneration complete!")


if __name__ == "__main__":
cli()
24 changes: 13 additions & 11 deletions getstream/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import time
import uuid
import asyncio
from typing import Any, Dict, Optional, Type, get_origin
from typing import Any, Dict, Optional, Type, cast, get_origin

from getstream.models import APIError
from getstream.rate_limit import extract_rate_limit
Expand All @@ -25,7 +25,7 @@
import ijson


def build_path(path: str, path_params: dict) -> str:
def build_path(path: str, path_params: Optional[Dict[str, Any]]) -> str:
if path_params is None:
return path
for k, v in path_params.items():
Expand All @@ -46,12 +46,14 @@ def _parse_response(
try:
parsed_result = json.loads(response.text) if response.text else {}

data: T
if hasattr(data_type, "from_dict"):
data = data_type.from_dict(parsed_result, infer_missing=True)
from_dict = getattr(data_type, "from_dict")
data = from_dict(parsed_result, infer_missing=True)
elif get_origin(data_type) is not dict:
raise AttributeError(f"{data_type.__name__} has no 'from_dict' method")
else:
data = parsed_result
data = cast(T, parsed_result)

except ValueError:
raise StreamAPIException(
Expand Down Expand Up @@ -140,7 +142,7 @@ def __init__(
user_agent=user_agent,
)
self.client = httpx.Client(
base_url=self.base_url,
base_url=self.base_url or "",
headers=self.headers,
params=self.params,
timeout=httpx.Timeout(self.timeout),
Expand All @@ -154,7 +156,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):

def _endpoint_name(self, path: str) -> str:
op = getattr(self, "_operation_name", None)
return op or current_operation(self._normalize_endpoint_from_path(path))
return op or current_operation(self._normalize_endpoint_from_path(path)) or ""

def _request_sync(
self,
Expand Down Expand Up @@ -315,25 +317,25 @@ def __init__(
user_agent=user_agent,
)
self.client = httpx.AsyncClient(
base_url=self.base_url,
base_url=self.base_url or "",
headers=self.headers,
params=self.params,
timeout=httpx.Timeout(self.timeout),
)

def __aenter__(self):
async def __aenter__(self):
return self

def __aexit__(self, exc_type, exc_val, exc_tb):
self.close()
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.aclose()

async def aclose(self):
"""Close HTTPX async client (closes pools/keep-alives)."""
await self.client.aclose()

def _endpoint_name(self, path: str) -> str:
op = getattr(self, "_operation_name", None)
return op or current_operation(self._normalize_endpoint_from_path(path))
return op or current_operation(self._normalize_endpoint_from_path(path)) or ""

async def _request_async(
self,
Expand Down
16 changes: 8 additions & 8 deletions getstream/common/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def span_request(
if tracer is None:
yield _NullSpan() # pragma: no cover
return
with tracer.start_as_current_span(name, kind=SpanKind.CLIENT) as span: # type: ignore[arg-type]
with tracer.start_as_current_span(name, kind=SpanKind.CLIENT) as span:
base_attrs: Dict[str, Any] = dict(attributes or {})
# auto-propagate contextual IDs to request spans
try:
Expand All @@ -190,22 +190,22 @@ def span_request(
pass
if base_attrs:
try:
span.set_attributes(base_attrs) # type: ignore[attr-defined]
span.set_attributes(base_attrs)
except Exception:
pass
if include_bodies and request_body is not None:
try:
span.add_event(
"request.body", {"level": "INFO", "body": safe_dump(request_body)}
) # type: ignore[attr-defined]
)
except Exception:
pass
try:
yield span
except BaseException as e:
try:
span.record_exception(e) # type: ignore[attr-defined]
span.set_status(Status(StatusCode.ERROR, str(e))) # type: ignore[attr-defined]
span.record_exception(e)
span.set_status(Status(StatusCode.ERROR, str(e)))
except Exception:
pass
raise
Expand Down Expand Up @@ -328,7 +328,7 @@ def start_as_current_span(
if tracer is None: # pragma: no cover
yield _NullSpan()
return
with tracer.start_as_current_span(name, kind=use_kind) as span: # type: ignore[arg-type]
with tracer.start_as_current_span(name, kind=use_kind) as span:
base_attrs: Dict[str, Any] = dict(attributes or {})
# auto-propagate contextual IDs
try:
Expand All @@ -342,7 +342,7 @@ def start_as_current_span(
pass
if base_attrs:
try:
span.set_attributes(base_attrs) # type: ignore[attr-defined]
span.set_attributes(base_attrs)
except Exception:
pass
yield span
Expand Down Expand Up @@ -372,7 +372,7 @@ def detach_context(token: Optional[object]) -> None:
if not _HAS_OTEL or otel_context is None or token is None: # pragma: no cover
return
try:
otel_context.detach(token)
otel_context.detach(token) # type: ignore[arg-type]
except Exception:
# Best-effort; do not raise during detach
pass
Expand Down
6 changes: 3 additions & 3 deletions getstream/stream_response.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Optional, Generic
from typing import Any, Dict, Optional, Generic
import typing

import httpx
Expand Down Expand Up @@ -27,9 +27,9 @@ def rate_limit(self) -> Optional[RateLimitInfo]:
"""Returns the ratelimit info of your API operation."""
return self.__rate_limit

def headers(self) -> typing.Dict[str, Any]:
def headers(self) -> Dict[str, Any]:
"""Returns the headers of the response."""
return self.__headers
return dict(self.__headers)

def status_code(self) -> int:
"""Returns the HTTP status code of the response."""
Expand Down
8 changes: 5 additions & 3 deletions getstream/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
import asyncio
from typing import Dict, List, Optional, Union
from typing import Dict, List, Optional, Union, cast
from urllib.parse import quote
from datetime import datetime
from datetime import timezone
Expand Down Expand Up @@ -77,10 +77,12 @@ def datetime_from_unix_ns(
return None

if isinstance(ts, dict):
return {k: datetime_from_unix_ns(v) for k, v in ts.items()}
# Values in dict are int/float/str, so recursive calls return datetime
return {k: cast(datetime, datetime_from_unix_ns(v)) for k, v in ts.items()}

if isinstance(ts, list):
return [datetime_from_unix_ns(v) for v in ts]
# Values in list are int/float/str, so recursive calls return datetime
return [cast(datetime, datetime_from_unix_ns(v)) for v in ts]

if isinstance(ts, str):
ts = int(ts)
Expand Down
Loading