Skip to content

Commit b084cbd

Browse files
roznawskflochtililochcursoragent
authored
Add support for python 3.10 (#78)
* feat: lower minimum Python version to 3.10 Replace asyncio.TaskGroup (Python 3.11+) in tests/test_notifier.py with asyncio.ensure_future and asyncio.gather, which are compatible with Python 3.10. Update requires-python in pyproject.toml and add 3.10 to the CI test matrix. Co-authored-by: Cursor <cursoragent@cursor.com> * ci: skip examples on 3.10 lane and harden test cleanup - Lower examples/*/pyproject.toml requires-python to >=3.10 so the workspace lockfile resolves on 3.10 (uv unifies requires-python across workspace members; any member at >=3.11 forces the lock to refuse 3.10). Example sources still target 3.11+ at runtime; they are not installed or type-checked on the 3.10 CI lane. - ci.yml: on the 3.10 matrix entry, sync without --all-packages and run pyright scoped to fishjam/ so the 3.11+ examples never reach the 3.10 venv. 3.11+ lanes are unchanged. - tests/test_notifier.py: wrap each async test in try/finally with asyncio.gather(*, return_exceptions=True) so background tasks are always cancelled and awaited, restoring the auto-cleanup semantics asyncio.TaskGroup provided. Replace the inconsistent try/except CancelledError pattern in two tests. - uv.lock regenerated against the new workspace constraint. * tests: replace asyncio.timeout(0) with await + suppress(CancelledError) asyncio.timeout is 3.11+. After task.cancel(), awaiting the task raises CancelledError once cancellation propagates; suppressing it is equivalent to the previous TimeoutError-via-timeout(0) idiom and works on 3.10. --------- Co-authored-by: flochtililoch <flochtililoch@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 91bbec2 commit b084cbd

9 files changed

Lines changed: 412 additions & 38 deletions

File tree

.github/workflows/ci.yml

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
runs-on: ubuntu-latest
2525
strategy: &strategy
2626
matrix:
27-
python-version: ["3.11", "3.12", "3.13"]
27+
python-version: ["3.10", "3.11", "3.12", "3.13"]
2828
steps:
2929
- uses: actions/checkout@v4
3030
- uses: astral-sh/setup-uv@v5
@@ -33,10 +33,20 @@ jobs:
3333
enable-cache: true
3434

3535
- name: Install project dependencies
36-
run: uv sync --locked --all-extras --all-packages
36+
run: |
37+
if [ "${{ matrix.python-version }}" = "3.10" ]; then
38+
uv sync --locked --all-extras
39+
else
40+
uv sync --locked --all-extras --all-packages
41+
fi
3742
3843
- name: Type check
39-
run: uv run pyright
44+
run: |
45+
if [ "${{ matrix.python-version }}" = "3.10" ]; then
46+
uv run pyright fishjam
47+
else
48+
uv run pyright
49+
fi
4050
4151
test:
4252
runs-on: ubuntu-latest
@@ -50,7 +60,12 @@ jobs:
5060
enable-cache: true
5161

5262
- name: Install project dependencies
53-
run: uv sync --locked --all-extras --all-packages
63+
run: |
64+
if [ "${{ matrix.python-version }}" = "3.10" ]; then
65+
uv sync --locked --all-extras
66+
else
67+
uv sync --locked --all-extras --all-packages
68+
fi
5469
5570
- name: Initialize Localtunnel
5671
id: tunnel

examples/multimodal/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "multimodal"
33
version = "0.1.0"
44
description = "Fishjam multimodal demo with Gemini Live API"
55
readme = "README.md"
6-
requires-python = ">=3.11"
6+
requires-python = ">=3.10"
77
dependencies = [
88
"fastapi[standard]==0.116.0",
99
"fishjam-server-sdk",

examples/poet_chat/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "poet_chat"
33
version = "0.1.0"
44
description = "Fishjam voice agent example"
55
readme = "README.md"
6-
requires-python = ">=3.11"
6+
requires-python = ">=3.10"
77
dependencies = ["fishjam-server-sdk", "openai-agents[voice]>=0.2.11"]
88

99
[tool.uv.sources]

examples/selective_subscription/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "selective-subscription-demo"
33
version = "0.1.0"
44
description = "Selective subscription demo using Fishjam Python SDK"
55
readme = "README.md"
6-
requires-python = ">=3.11"
6+
requires-python = ">=3.10"
77
dependencies = [
88
"starlette>=0.35.0",
99
"uvicorn>=0.25.0",

examples/transcription/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "transcription"
33
version = "0.1.0"
44
description = "Fishjam transcription demo"
55
readme = "README.md"
6-
requires-python = ">=3.11"
6+
requires-python = ">=3.10"
77
dependencies = [
88
"fastapi[standard]==0.116.0",
99
"fishjam-server-sdk",

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "fishjam-server-sdk"
33
version = "0.26.0"
44
description = "Python server SDK for the Fishjam"
55
authors = [{ name = "Fishjam Team", email = "contact@fishjam.io" }]
6-
requires-python = ">=3.11"
6+
requires-python = ">=3.10"
77
readme = "README.md"
88
license = "Apache-2.0"
99
dependencies = [

tests/agent/test_agent.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,8 @@ def _(notification: AllowedNotification):
5454
yield notifier
5555

5656
task.cancel()
57-
with suppress(asyncio.TimeoutError):
58-
async with asyncio.timeout(0):
59-
await task
57+
with suppress(asyncio.CancelledError):
58+
await task
6059

6160

6261
class TestAgentApi:

tests/test_notifier.py

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,17 @@ async def test_valid_credentials(self):
8080
def handle_notitifcation(_notification):
8181
pass
8282

83-
async with asyncio.TaskGroup() as tg:
84-
notifier_task = tg.create_task(notifier.connect())
83+
notifier_task = asyncio.ensure_future(notifier.connect())
84+
try:
8585
await notifier.wait_ready()
8686

8787
assert (
8888
notifier._websocket
8989
and notifier._websocket.state == websockets.State.OPEN
9090
)
91-
91+
finally:
9292
notifier_task.cancel()
93+
await asyncio.gather(notifier_task, return_exceptions=True)
9394

9495

9596
@pytest.fixture
@@ -114,10 +115,11 @@ async def test_room_created_deleted(
114115
):
115116
event_checks = [ServerMessageRoomCreated, ServerMessageRoomDeleted]
116117

117-
async with asyncio.TaskGroup() as tg:
118-
assert_task = tg.create_task(assert_events(notifier, event_checks.copy()))
119-
120-
notifier_task = tg.create_task(notifier.connect())
118+
assert_task = asyncio.ensure_future(
119+
assert_events(notifier, event_checks.copy())
120+
)
121+
notifier_task = asyncio.ensure_future(notifier.connect())
122+
try:
121123
await notifier.wait_ready()
122124

123125
options = RoomOptions(webhook_url=WEBHOOK_URL)
@@ -126,8 +128,10 @@ async def test_room_created_deleted(
126128
room_api.delete_room(room.id)
127129

128130
await assert_task
129-
131+
finally:
132+
assert_task.cancel()
130133
notifier_task.cancel()
134+
await asyncio.gather(assert_task, notifier_task, return_exceptions=True)
131135

132136
self.assert_webhook_events(event_checks, event_queue, room.id)
133137

@@ -144,28 +148,32 @@ async def test_peer_connected_disconnected(
144148
ServerMessageRoomDeleted,
145149
]
146150

147-
async with asyncio.TaskGroup() as tg:
148-
assert_task = tg.create_task(assert_events(notifier, event_checks.copy()))
149-
150-
notifier_task = tg.create_task(notifier.connect())
151+
assert_task = asyncio.ensure_future(
152+
assert_events(notifier, event_checks.copy())
153+
)
154+
notifier_task = asyncio.ensure_future(notifier.connect())
155+
tasks = [assert_task, notifier_task]
156+
try:
151157
await notifier.wait_ready()
152158

153159
options = RoomOptions(webhook_url=WEBHOOK_URL)
154160
room = room_api.create_room(options=options)
155161

156162
peer, token = room_api.create_peer(room.id)
157163
peer_socket = PeerSocket(fishjam_url=FISHJAM_ID)
158-
peer_socket_task = tg.create_task(peer_socket.connect(token))
164+
peer_socket_task = asyncio.ensure_future(peer_socket.connect(token))
165+
tasks.append(peer_socket_task)
159166

160167
await peer_socket.wait_ready()
161168

162169
room_api.delete_peer(room.id, peer.id)
163170
room_api.delete_room(room.id)
164171

165172
await assert_task
166-
167-
notifier_task.cancel()
168-
peer_socket_task.cancel()
173+
finally:
174+
for task in tasks:
175+
task.cancel()
176+
await asyncio.gather(*tasks, return_exceptions=True)
169177

170178
self.assert_webhook_events(event_checks, event_queue, room.id)
171179

@@ -181,27 +189,31 @@ async def test_peer_connected_room_deleted(
181189
ServerMessageRoomDeleted,
182190
]
183191

184-
async with asyncio.TaskGroup() as tg:
185-
assert_task = tg.create_task(assert_events(notifier, event_checks.copy()))
186-
187-
notifier_task = tg.create_task(notifier.connect())
192+
assert_task = asyncio.ensure_future(
193+
assert_events(notifier, event_checks.copy())
194+
)
195+
notifier_task = asyncio.ensure_future(notifier.connect())
196+
tasks = [assert_task, notifier_task]
197+
try:
188198
await notifier.wait_ready()
189199

190200
options = RoomOptions(webhook_url=WEBHOOK_URL)
191201
room = room_api.create_room(options=options)
192202
_peer, token = room_api.create_peer(room.id)
193203

194204
peer_socket = PeerSocket(fishjam_url=FISHJAM_ID)
195-
peer_socket_task = tg.create_task(peer_socket.connect(token))
205+
peer_socket_task = asyncio.ensure_future(peer_socket.connect(token))
206+
tasks.append(peer_socket_task)
196207

197208
await peer_socket.wait_ready()
198209

199210
room_api.delete_room(room.id)
200211

201212
await assert_task
202-
203-
notifier_task.cancel()
204-
peer_socket_task.cancel()
213+
finally:
214+
for task in tasks:
215+
task.cancel()
216+
await asyncio.gather(*tasks, return_exceptions=True)
205217

206218
self.assert_webhook_events(event_checks, event_queue, room.id)
207219

0 commit comments

Comments
 (0)