realtime developemnt started STILL LOTS OF BUGS
This commit is contained in:
parent
8363b315c3
commit
78010eafcf
@ -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
|
||||||
|
|||||||
@ -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"}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user