realtime developemnt started STILL LOTS OF BUGS

This commit is contained in:
Vithor Jaeger 2022-09-22 18:00:49 -04:00
parent 8363b315c3
commit 78010eafcf
5 changed files with 124 additions and 28 deletions

View File

@ -1,6 +1,6 @@
__title__ = "pocketbase" __title__ = "pocketbase"
__description__ = "PocketBase client SDK for python." __description__ = "PocketBase client SDK for python."
__version__ = "0.1.2" __version__ = "0.1.3"
from .client import Client, ClientResponseError from .client import Client, ClientResponseError

View File

@ -19,7 +19,7 @@ class ClientResponseError(Exception):
status: int = 0 status: int = 0
data: dict = {} data: dict = {}
is_abort: bool = False is_abort: bool = False
original_error: Any = None original_error: Any | None = None
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args) super().__init__(*args)
@ -46,7 +46,7 @@ class Client:
self, self,
base_url: str = "/", base_url: str = "/",
lang: str = "en-US", lang: str = "en-US",
auth_store: BaseAuthStore = None, auth_store: BaseAuthStore | None = None,
) -> None: ) -> None:
self.base_url = base_url self.base_url = base_url
self.lang = lang self.lang = lang
@ -60,12 +60,6 @@ class Client:
self.settings = Settings(self) self.settings = Settings(self)
self.realtime = Realtime(self) self.realtime = Realtime(self)
def cancel_request(self, cancel_key: str):
return self
def cancel_all_requests(self):
return self
def send(self, path: str, req_config: dict[str:Any]) -> Any: def send(self, path: str, req_config: dict[str:Any]) -> Any:
"""Sends an api http request.""" """Sends an api http request."""
config = {"method": "GET"} config = {"method": "GET"}

View File

@ -1,19 +1,46 @@
from __future__ import annotations from __future__ import annotations
from typing import Callable, Optional from typing import Callable
import dataclasses
import json
from pocketbase.services.utils.base_service import BaseService from pocketbase.services.utils.base_service import BaseService
from pocketbase.services.utils.sse import Event, SSEClient
from pocketbase.models.record import Record from pocketbase.models.record import Record
@dataclasses.dataclass
class MessageData:
action: str
record: Record
class Realtime(BaseService): class Realtime(BaseService):
client_id: str
subscriptions: dict subscriptions: dict
client_id: str = ""
event_source: SSEClient | None = None
def subscribe(self, subscription: str, callback: Callable) -> None: def __init__(self, client) -> None:
super().__init__(client)
self.subscriptions = {}
self.client_id = ""
self.event_source = None
def subscribe(
self, subscription: str, callback: Callable[[MessageData], None]
) -> None:
"""Inits the sse connection (if not already) and register the subscription.""" """Inits the sse connection (if not already) and register the subscription."""
self.subscriptions[subscription] = callback # unsubscribe existing
if subscription in self.subscriptions and self.event_source:
self.event_source.remove_event_listener(subscription, callback)
# register subscription
self.subscriptions[subscription] = self._make_subscription(callback)
if not self.event_source:
self._connect()
elif self.client_id:
self._submit_subscriptions()
def unsubscribe(self, subscription: Optional[str] = None) -> None: def unsubscribe(self, subscription: str | None = None) -> None:
""" """
Unsubscribe from a subscription. Unsubscribe from a subscription.
@ -23,29 +50,79 @@ class Realtime(BaseService):
The related sse connection will be autoclosed if after the The related sse connection will be autoclosed if after the
unsubscribe operations there are no active subscriptions left. unsubscribe operations there are no active subscriptions left.
""" """
pass if not subscription:
self._remove_subscription_listeners()
self.subscriptions = {}
elif subscription in self.subscriptions:
self.event_source.remove_event_listener(
subscription, self.subscriptions[subscription]
)
self.subscriptions.pop(subscription)
else:
return
if self.client_id:
self._submit_subscriptions()
if not self.subscriptions:
self._disconnect()
def _make_subscription(
self, callback: Callable[[MessageData], None]
) -> Callable[[Event], None]:
def listener(event: Event) -> None:
data = json.loads(event.data)
if "record" in data and "action" in data:
callback(
MessageData(
action=data["action"],
record=Record(
data=data["record"],
),
)
)
return listener
def _submit_subscriptions(self) -> bool: def _submit_subscriptions(self) -> bool:
self._add_subscription_listeners()
self.client.send( self.client.send(
"/api/realtime", "/api/realtime",
{ {
"method": "POST", "method": "POST",
"body": { "body": {
"clientId": self.client_id, "clientId": self.client_id,
"subscriptions": self.subscriptions.keys(), "subscriptions": list(self.subscriptions.keys()),
}, },
}, },
) )
return True return True
def _add_subscription_listeners(self) -> None: def _add_subscription_listeners(self) -> None:
pass if not self.event_source:
return
self._remove_subscription_listeners()
for subscription, callback in self.subscriptions.items():
self.event_source.add_event_listener(subscription, callback)
def _remove_subscription_listeners(self) -> None: def _remove_subscription_listeners(self) -> None:
pass if not self.event_source:
return
for subscription, callback in self.subscriptions.items():
self.event_source.remove_event_listener(subscription, callback)
def _connect_handler(self, event: Event) -> None:
self.client_id = event.id
self._submit_subscriptions()
def _connect(self) -> None: def _connect(self) -> None:
pass self._disconnect()
self.event_source = SSEClient(self.client.build_url("/api/realtime"))
self.event_source.add_event_listener("PB_CONNECT", self._connect_handler)
def _disconnect(self) -> None: def _disconnect(self) -> None:
pass self._remove_subscription_listeners()
self.client_id = ""
if not self.event_source:
return
self.event_source.remove_event_listener("PB_CONNECT", self._connect_handler)
self.event_source.close()
self.event_source = None

View File

@ -1,11 +1,13 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from typing import Callable
import dataclasses
import asyncio
import httpx import httpx
@dataclass @dataclasses.dataclass
class Event: class Event:
"""Representation of an event""" """Representation of an event"""
@ -19,6 +21,8 @@ class SSEClient:
"""Implementation of a server side event client""" """Implementation of a server side event client"""
FIELD_SEPARATOR = ":" FIELD_SEPARATOR = ":"
_listeners: dict = {}
_loop_running: bool = False
def __init__( def __init__(
self, self,
@ -33,14 +37,15 @@ class SSEClient:
self.headers = headers self.headers = headers
self.payload = payload self.payload = payload
self.encoding = encoding self.encoding = encoding
self.client = httpx.AsyncClient()
def _read(self): async def _read(self):
"""Read the incoming event source stream and yield event chunks""" """Read the incoming event source stream and yield event chunks"""
data = b"" data = b""
with httpx.stream( async with self.client.stream(
self.method, self.url, headers=self.headers, data=self.payload, timeout=None self.method, self.url, headers=self.headers, data=self.payload, timeout=None
) as r: ) as r:
for chunk in r.iter_bytes(): async for chunk in r.aiter_bytes():
for line in chunk.splitlines(True): for line in chunk.splitlines(True):
data += line data += line
if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")):
@ -49,8 +54,8 @@ class SSEClient:
if data: if data:
yield data yield data
def events(self): async def _events(self):
for chunk in self._read(): async for chunk in self._read():
event = Event() event = Event()
for line in chunk.splitlines(): for line in chunk.splitlines():
line = line.decode(self.encoding) line = line.decode(self.encoding)
@ -77,3 +82,23 @@ class SSEClient:
event.data = event.data[0:-1] event.data = event.data[0:-1]
event.event = event.event or "message" event.event = event.event or "message"
yield event yield event
async def _loop(self):
self._loop_running = True
async for event in self._events():
if event.event in self._listeners:
self._listeners[event.event](event)
def add_event_listener(self, event: str, callback: Callable[[Event], None]) -> None:
self._listeners[event] = callback
if not self._loop_running:
asyncio.run(self._loop())
def remove_event_listener(
self, event: str, callback: Callable[[Event], None]
) -> None:
if event in self._listeners:
self._listeners.pop(event)
def close(self) -> None:
pass

View File

@ -28,7 +28,7 @@ dynamic = ["readme", "version"]
[tool.poetry] [tool.poetry]
name = "pocketbase" name = "pocketbase"
version = "0.1.2" version = "0.1.3"
description = "PocketBase SDK for python." description = "PocketBase SDK for python."
authors = ["Vithor Jaeger <vaphes@gmail.com>"] authors = ["Vithor Jaeger <vaphes@gmail.com>"]
readme = "README.md" readme = "README.md"