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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: ci, default, pre-commit, clean, start-infra, stop-infra
.PHONY: ci, default, clean, start-infra, stop-infra

PACKAGES := functions realtime storage auth postgrest supabase
FORALL_PKGS = $(foreach pkg, $(PACKAGES), $(pkg).$(1))
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ cd supabase-py

This repository relies on the following dependencies for development:
- `uv` for python project management.
- `make` for command running.
- `make` for running project commands.
- `docker` for both `postgrest` and `auth` test containers.
- `supabase-cli` for both `storage` and `realtime` test containers.

All of these dependencies are included in the nix shell environment, through `flake.nix`. If you've got `nix` installed, you may prefer to use it through `nix develop`.

### Use a Virtual Environment

We recommend using a virtual environment, preferrably through `uv`, given it is currently the only tool that understands the workspace setup (you can read more about it in [the uv docs](https://docs.astral.sh/uv/concepts/projects/workspaces/)).
We recommend using a virtual environment, preferably through `uv`, given it is currently the only tool that understands the workspace setup (you can read more about it in [the uv docs](https://docs.astral.sh/uv/concepts/projects/workspaces/)).

```
uv venv supabase-py
Expand All @@ -53,7 +53,7 @@ If you're using nix, the generated `python` executable should have the correct d

### Running tests and other commands

We use `make` to store and run the relevant commands. The structure is setup such that each sub package can individually set its command in its own `Makefile`, and the job of the main `Makefile` is just coordinate calling each of them.
We use `make` to store and run the relevant commands. The structure is set up such that each sub package can individually set its command in its own `Makefile`, and the job of the main `Makefile` is just coordinate calling each of them.

For instance, in order to run all tests of all packages, you should use the following root command
```bash
Expand All @@ -70,11 +70,11 @@ To run each of the packages' tests in parallel. This should be generally faster
Other relevant commands include
```bash
make install-hooks # install all commit hooks into the local .git folder
make stop-infra # stops all running containers from all packages
make clean # delete all intermediary files created by testing
make stop-infra # stops all running containers from all packages
make clean # delete all intermediary files created by testing
```
All the sub packages command are available from the main root by prefixing the command with `{package_name}.`. Examples:
All the subpackages command are available from the main root by prefixing the command with `{package_name}.`. Examples:
```bash
make realtime.tests # run only realtime tests
make storage.clean # delete temporary files only in the storage package
make storage.clean # delete temporary files only in the storage package
```
1 change: 0 additions & 1 deletion src/functions/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ tests = [
lints = [
"unasync>=0.6.0",
"ruff >=0.12.1",
"pre-commit >=3.4,<5.0",
"python-lsp-server (>=1.12.2,<2.0.0)",
"pylsp-mypy (>=0.7.0,<0.8.0)",
"python-lsp-ruff (>=2.2.2,<3.0.0)",
Expand Down
1 change: 0 additions & 1 deletion src/postgrest/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ test = [
"unasync >= 0.6.0",
]
lints = [
"pre-commit >=4.2.0",
"ruff >=0.12.1",
"python-lsp-server (>=1.12.2,<2.0.0)",
"pylsp-mypy (>=0.7.0,<0.8.0)",
Expand Down
30 changes: 23 additions & 7 deletions src/realtime/Makefile
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
.PHONY: pytest pre-commit mypy tests infra stop-infra
help::
@echo "Available commands"
@echo " help -- (default) print this message"

tests: mypy pytest
help::
@echo " tests -- run all tests for realtime package"

mypy:
uv run --package realtime mypy src/realtime
uv run --package realtime mypy src/realtime tests
help::
@echo " pytest -- run mypy on realtime package"

pytest: start-infra
uv run --package realtime pytest --cov=realtime --cov-report=xml --cov-report=html -vv
help::
@echo " pytest -- run pytest on realtime package"

start-infra:
supabase start --workdir infra -x studio,mailpit,edge-runtime,logflare,vector,supavisor,imgproxy,storage-api
help::
@echo " stop-infra -- start containers for tests"

stop-infra:
supabase --workdir infra stop

tests: mypy pytest

pytest: start-infra
uv run --package realtime pytest --cov=realtime --cov-report=xml --cov-report=html -vv
help::
@echo " stop-infra -- stop containers for tests"

clean:
rm -rf htmlcov .pytest_cache .mypy_cache .ruff_cache
rm -f .coverage coverage.xml
help::
@echo " clean -- clean intermediary files generated by tests"

build:
uv build --package realtime
help::
@echo " build -- invoke uv build on realtime package"
1 change: 0 additions & 1 deletion src/realtime/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ tests = [
"pytest-asyncio >= 1.0.0",
]
lints = [
"pre-commit >= 4.2.0",
"ruff >= 0.12.1",
"python-lsp-server (>=1.12.2,<2.0.0)",
"pylsp-mypy (>=0.7.0,<0.8.0)",
Expand Down
2 changes: 1 addition & 1 deletion src/realtime/src/realtime/_async/timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


class AsyncTimer:
def __init__(self, callback: Callable, timer_calc: Callable[[int], int]):
def __init__(self, callback: Callable, timer_calc: Callable[[int], float]):
self.callback = callback
self.timer_calc = timer_calc
self.timer: Optional[asyncio.Task] = None
Expand Down
Empty file.
100 changes: 75 additions & 25 deletions src/realtime/tests/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
import aiohttp
import pytest
from dotenv import load_dotenv
from pydantic import BaseModel

from realtime import AsyncRealtimeChannel, AsyncRealtimeClient, RealtimeSubscribeStates
from realtime import (
AsyncRealtimeChannel,
AsyncRealtimeClient,
RealtimePostgresChangesListenEvent,
RealtimeSubscribeStates,
)
from realtime.message import Message
from realtime.types import DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_TIMEOUT, ChannelEvents

Expand All @@ -27,6 +33,14 @@ def socket() -> AsyncRealtimeClient:
return AsyncRealtimeClient(url, key)


class SignupMessageResponse(BaseModel):
access_token: str
token_type: str
expires_in: int
expires_at: int
refresh_token: str


async def access_token() -> str:
url = f"{URL}/auth/v1/signup"
headers = {"apikey": ANON_KEY, "Content-Type": "application/json"}
Expand All @@ -39,8 +53,11 @@ async def access_token() -> str:
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=data) as response:
if response.status == 200:
json_response = await response.json()
return json_response.get("access_token")
response_content = await response.read()
signup_response = SignupMessageResponse.model_validate_json(
response_content
)
return signup_response.access_token
else:
raise Exception(
f"Failed to get access token. Status: {response.status}"
Expand All @@ -67,14 +84,20 @@ async def test_broadcast_events(socket: AsyncRealtimeClient):
await socket.connect()

channel = socket.channel(
"test-broadcast", params={"config": {"broadcast": {"self": True}}}
"test-broadcast",
params={
"config": {
"broadcast": {"self": True, "ack": True},
"presence": {"enabled": True, "key": ""},
"private": False,
}
},
)
received_events = []

semaphore = asyncio.Semaphore(0)

def broadcast_callback(payload):
print("broadcast: ", payload)
received_events.append(payload)
semaphore.release()

Expand Down Expand Up @@ -111,41 +134,50 @@ async def test_postgrest_changes(socket: AsyncRealtimeClient):
await socket.set_auth(token)

channel: AsyncRealtimeChannel = socket.channel("test-postgres-changes")
received_events = {"all": [], "insert": [], "update": [], "delete": []}
received_events: dict[str, list[dict]] = {
"all": [],
"insert": [],
"update": [],
"delete": [],
}

def all_changes_callback(payload):
print("all_changes_callback: ", payload)
received_events["all"].append(payload)

insert_event = asyncio.Event()

def insert_callback(payload):
print("insert_callback: ", payload)
received_events["insert"].append(payload)
insert_event.set()

update_event = asyncio.Event()

def update_callback(payload):
print("update_callback: ", payload)
received_events["update"].append(payload)
update_event.set()

delete_event = asyncio.Event()

def delete_callback(payload):
print("delete_callback: ", payload)
received_events["delete"].append(payload)
delete_event.set()

subscribed_event = asyncio.Event()
system_event = asyncio.Event()

await (
channel.on_postgres_changes("*", all_changes_callback, table="todos")
.on_postgres_changes("INSERT", insert_callback, table="todos")
.on_postgres_changes("UPDATE", update_callback, table="todos")
.on_postgres_changes("DELETE", delete_callback, table="todos")
channel.on_postgres_changes(
RealtimePostgresChangesListenEvent.All, all_changes_callback, table="todos"
)
.on_postgres_changes(
RealtimePostgresChangesListenEvent.Insert, insert_callback, table="todos"
)
.on_postgres_changes(
RealtimePostgresChangesListenEvent.Update, update_callback, table="todos"
)
.on_postgres_changes(
RealtimePostgresChangesListenEvent.Delete, delete_callback, table="todos"
)
.on_system(lambda _: system_event.set())
.subscribe(
lambda state, _: (
Expand Down Expand Up @@ -206,26 +238,30 @@ async def test_postgrest_changes_on_different_tables(socket: AsyncRealtimeClient
await socket.set_auth(token)

channel: AsyncRealtimeChannel = socket.channel("test-postgres-changes")
received_events = {"all": [], "insert": []}
received_events: dict[str, list[dict]] = {"all": [], "insert": []}

def all_changes_callback(payload):
print("all_changes_callback: ", payload)
received_events["all"].append(payload)

insert_event = asyncio.Event()

def insert_callback(payload):
print("insert_callback: ", payload)
received_events["insert"].append(payload)
insert_event.set()

subscribed_event = asyncio.Event()
system_event = asyncio.Event()

await (
channel.on_postgres_changes("*", all_changes_callback, table="todos")
.on_postgres_changes("INSERT", insert_callback, table="todos")
.on_postgres_changes("INSERT", insert_callback, table="messages")
channel.on_postgres_changes(
RealtimePostgresChangesListenEvent.All, all_changes_callback, table="todos"
)
.on_postgres_changes(
RealtimePostgresChangesListenEvent.Insert, insert_callback, table="todos"
)
.on_postgres_changes(
RealtimePostgresChangesListenEvent.Insert, insert_callback, table="messages"
)
.on_system(lambda _: system_event.set())
.subscribe(
lambda state, _: (
Expand Down Expand Up @@ -273,6 +309,10 @@ def insert_callback(payload):
await socket.close()


class CreateTodoResponse(BaseModel):
id: str


async def create_todo(access_token: str, todo: dict) -> str:
url = f"{URL}/rest/v1/todos?select=id"
headers = {
Expand All @@ -286,8 +326,11 @@ async def create_todo(access_token: str, todo: dict) -> str:
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=todo) as response:
if response.status == 201:
json_response = await response.json()
return json_response.get("id")
response_content = await response.read()
create_todo_response = CreateTodoResponse.model_validate_json(
response_content
)
return create_todo_response.id
else:
raise Exception(f"Failed to create todo. Status: {response.status}")

Expand All @@ -306,7 +349,11 @@ async def update_todo(access_token: str, id: str, todo: dict):
raise Exception(f"Failed to update todo. Status: {response.status}")


async def create_message(access_token: str, message: dict) -> str:
class CreateMsgResponse(BaseModel):
id: int


async def create_message(access_token: str, message: dict) -> int:
url = f"{URL}/rest/v1/messages?select=id"
headers = {
"apikey": ANON_KEY,
Expand All @@ -319,8 +366,11 @@ async def create_message(access_token: str, message: dict) -> str:
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=message) as response:
if response.status == 201:
json_response = await response.json()
return json_response.get("id")
response_content = await response.read()
create_msg_response = CreateMsgResponse.model_validate_json(
response_content
)
return create_msg_response.id
else:
raise Exception(f"Failed to create message. Status: {response.status}")

Expand Down
Loading