pulsegrid 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: pulsegrid
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the PulseGrid realtime platform
5
+ Author: Kwandile Mofokeng
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Kwandiletshepo/pulsegrid-python
8
+ Project-URL: Repository, https://github.com/Kwandiletshepo/pulsegrid-python
9
+ Project-URL: Issues, https://github.com/Kwandiletshepo/pulsegrid-python/issues
10
+ Keywords: pulsegrid,realtime,websocket,channels,presence,sdk
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: requests>=2.31.0
22
+ Requires-Dist: websocket-client>=1.8.0
23
+
24
+ # PulseGrid Python SDK
25
+
26
+ Official Python SDK for PulseGrid.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install pulsegrid
@@ -0,0 +1,8 @@
1
+ # PulseGrid Python SDK
2
+
3
+ Official Python SDK for PulseGrid.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install pulsegrid
@@ -0,0 +1,4 @@
1
+ from .client import create_pulsegrid, PulseGridClient
2
+
3
+ __all__ = ["create_pulsegrid", "PulseGridClient"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,364 @@
1
+ import json
2
+ from typing import Any, Callable, Dict, Optional
3
+
4
+ import requests
5
+ from websocket import WebSocketApp
6
+
7
+
8
+ def create_pulsegrid(
9
+ *,
10
+ base_url: str,
11
+ public_key: str,
12
+ secret_key: str,
13
+ project_id: str,
14
+ ) -> "PulseGridClient":
15
+ return PulseGridClient(
16
+ base_url=base_url,
17
+ public_key=public_key,
18
+ secret_key=secret_key,
19
+ project_id=project_id,
20
+ )
21
+
22
+
23
+ class PulseGridClient:
24
+ def __init__(
25
+ self,
26
+ *,
27
+ base_url: str,
28
+ public_key: str,
29
+ secret_key: str,
30
+ project_id: str,
31
+ ) -> None:
32
+ self.base_url = (base_url or "").rstrip("/")
33
+ self.public_key = public_key or ""
34
+ self.secret_key = secret_key or ""
35
+ self.project_id = project_id or ""
36
+
37
+ if not self.base_url:
38
+ raise ValueError("base_url is required.")
39
+ if not self.public_key:
40
+ raise ValueError("public_key is required.")
41
+ if not self.secret_key:
42
+ raise ValueError("secret_key is required.")
43
+ if not self.project_id:
44
+ raise ValueError("project_id is required.")
45
+
46
+ self.projects = ProjectHelpers(self)
47
+ self.tokens = TokenHelpers(self)
48
+
49
+ def _headers(self, *, json_body: bool = False) -> Dict[str, str]:
50
+ headers = {
51
+ "X-PulseGrid-Public-Key": self.public_key,
52
+ "X-PulseGrid-Secret-Key": self.secret_key,
53
+ }
54
+ if json_body:
55
+ headers["Content-Type"] = "application/json"
56
+ return headers
57
+
58
+ def _read_json(self, response: requests.Response) -> Dict[str, Any]:
59
+ try:
60
+ data = response.json()
61
+ except ValueError as exc:
62
+ raise RuntimeError(
63
+ f"PulseGrid returned a non-JSON response with status {response.status_code}."
64
+ ) from exc
65
+
66
+ if not response.ok:
67
+ message = (
68
+ data.get("error", {}).get("message")
69
+ or data.get("message")
70
+ or f"PulseGrid request failed with status {response.status_code}."
71
+ )
72
+ raise RuntimeError(message)
73
+
74
+ return data
75
+
76
+ def _ws_base(self) -> str:
77
+ if self.base_url.startswith("https://"):
78
+ return self.base_url.replace("https://", "wss://", 1)
79
+ if self.base_url.startswith("http://"):
80
+ return self.base_url.replace("http://", "ws://", 1)
81
+ return self.base_url
82
+
83
+ def test_auth(self) -> Dict[str, Any]:
84
+ response = requests.post(
85
+ f"{self.base_url}/api/auth/test/",
86
+ headers=self._headers(),
87
+ timeout=30,
88
+ )
89
+ return self._read_json(response)
90
+
91
+ def channel(self, channel_slug: str) -> "PulseGridChannel":
92
+ if not channel_slug or not channel_slug.strip():
93
+ raise ValueError("channel_slug is required.")
94
+ return PulseGridChannel(self, channel_slug.strip())
95
+
96
+
97
+ class ProjectHelpers:
98
+ def __init__(self, client: PulseGridClient) -> None:
99
+ self.client = client
100
+
101
+ def list(self) -> Dict[str, Any]:
102
+ response = requests.get(
103
+ f"{self.client.base_url}/api/projects/",
104
+ headers=self.client._headers(),
105
+ timeout=30,
106
+ )
107
+ return self.client._read_json(response)
108
+
109
+
110
+ class TokenHelpers:
111
+ def __init__(self, client: PulseGridClient) -> None:
112
+ self.client = client
113
+
114
+ def create(
115
+ self,
116
+ *,
117
+ identifier_id: str,
118
+ identifier_label: str = "",
119
+ allowed_channels: Optional[list[str]] = None,
120
+ expires_in: int = 3600,
121
+ metadata: Optional[Dict[str, Any]] = None,
122
+ ) -> str:
123
+ if not identifier_id or not identifier_id.strip():
124
+ raise ValueError("identifier_id is required.")
125
+
126
+ payload = {
127
+ "project_id": self.client.project_id,
128
+ "identifier_id": identifier_id.strip(),
129
+ "identifier_label": identifier_label,
130
+ "allowed_channels": allowed_channels or [],
131
+ "expires_in": expires_in,
132
+ "metadata": metadata or {},
133
+ }
134
+
135
+ response = requests.post(
136
+ f"{self.client.base_url}/api/client-tokens/create/",
137
+ headers=self.client._headers(json_body=True),
138
+ data=json.dumps(payload),
139
+ timeout=30,
140
+ )
141
+
142
+ result = self.client._read_json(response)
143
+ return result["data"]["client_token"]["token"]
144
+
145
+
146
+ class PulseGridChannel:
147
+ def __init__(self, client: PulseGridClient, channel_slug: str) -> None:
148
+ self.client = client
149
+ self.channel_slug = channel_slug
150
+
151
+ def list(self) -> Dict[str, Any]:
152
+ response = requests.get(
153
+ f"{self.client.base_url}/api/channels/",
154
+ headers=self.client._headers(),
155
+ params={"project_id": self.client.project_id},
156
+ timeout=30,
157
+ )
158
+ return self.client._read_json(response)
159
+
160
+ def create(
161
+ self,
162
+ *,
163
+ name: str,
164
+ channel_type: str = "public",
165
+ description: str = "",
166
+ persist_events: bool = True,
167
+ retention_days: int = 30,
168
+ ) -> Dict[str, Any]:
169
+ if not name or not name.strip():
170
+ raise ValueError("name is required.")
171
+
172
+ payload = {
173
+ "project_id": self.client.project_id,
174
+ "name": name.strip(),
175
+ "channel_type": channel_type,
176
+ "description": description,
177
+ "persist_events": persist_events,
178
+ "retention_days": retention_days,
179
+ }
180
+
181
+ response = requests.post(
182
+ f"{self.client.base_url}/api/channels/create/",
183
+ headers=self.client._headers(json_body=True),
184
+ data=json.dumps(payload),
185
+ timeout=30,
186
+ )
187
+ return self.client._read_json(response)
188
+
189
+ def publish(
190
+ self,
191
+ text: str,
192
+ *,
193
+ identifier_id: str = "server",
194
+ identifier_label: str = "Server",
195
+ event: str = "message",
196
+ meta: Optional[Dict[str, Any]] = None,
197
+ data: Optional[Dict[str, Any]] = None,
198
+ ) -> Dict[str, Any]:
199
+ if not text or not str(text).strip():
200
+ raise ValueError("text is required.")
201
+
202
+ payload = {
203
+ "project_id": self.client.project_id,
204
+ "channel_slug": self.channel_slug,
205
+ "event": event,
206
+ "identifier_id": identifier_id,
207
+ "identifier_label": identifier_label,
208
+ "data": {
209
+ "text": str(text).strip(),
210
+ **(data or {}),
211
+ },
212
+ "meta": meta or {},
213
+ }
214
+
215
+ response = requests.post(
216
+ f"{self.client.base_url}/api/messages/publish/",
217
+ headers=self.client._headers(json_body=True),
218
+ data=json.dumps(payload),
219
+ timeout=30,
220
+ )
221
+ return self.client._read_json(response)
222
+
223
+ def presence(self) -> Dict[str, Any]:
224
+ response = requests.get(
225
+ f"{self.client.base_url}/api/channels/presence/",
226
+ headers=self.client._headers(),
227
+ params={
228
+ "project_id": self.client.project_id,
229
+ "channel_slug": self.channel_slug,
230
+ },
231
+ timeout=30,
232
+ )
233
+ return self.client._read_json(response)
234
+
235
+ def online_count(self) -> int:
236
+ result = self.presence()
237
+ return result["data"]["presence"]["online_count"]
238
+
239
+ def is_online(self, identifier_id: str) -> Dict[str, Any]:
240
+ if not identifier_id or not identifier_id.strip():
241
+ raise ValueError("identifier_id is required.")
242
+
243
+ response = requests.get(
244
+ f"{self.client.base_url}/api/channels/presence/check/",
245
+ headers=self.client._headers(),
246
+ params={
247
+ "project_id": self.client.project_id,
248
+ "channel_slug": self.channel_slug,
249
+ "identifier_id": identifier_id.strip(),
250
+ },
251
+ timeout=30,
252
+ )
253
+ return self.client._read_json(response)
254
+
255
+ def connect(
256
+ self,
257
+ *,
258
+ client_token: str,
259
+ on_open: Optional[Callable[[], None]] = None,
260
+ on_message: Optional[Callable[[Dict[str, Any]], None]] = None,
261
+ on_close: Optional[Callable[[int, str], None]] = None,
262
+ on_error: Optional[Callable[[Exception], None]] = None,
263
+ ) -> "PulseGridConnection":
264
+ if not client_token or not client_token.strip():
265
+ raise ValueError("client_token is required.")
266
+
267
+ ws_url = (
268
+ f"{self.client._ws_base()}/ws/channels/"
269
+ f"{self.client.project_id}/{self.channel_slug}/?token={client_token}"
270
+ )
271
+
272
+ return PulseGridConnection(
273
+ ws_url=ws_url,
274
+ on_open=on_open,
275
+ on_message=on_message,
276
+ on_close=on_close,
277
+ on_error=on_error,
278
+ )
279
+
280
+
281
+ class PulseGridConnection:
282
+ def __init__(
283
+ self,
284
+ *,
285
+ ws_url: str,
286
+ on_open: Optional[Callable[[], None]] = None,
287
+ on_message: Optional[Callable[[Dict[str, Any]], None]] = None,
288
+ on_close: Optional[Callable[[int, str], None]] = None,
289
+ on_error: Optional[Callable[[Exception], None]] = None,
290
+ ) -> None:
291
+ self.ws_url = ws_url
292
+ self._user_on_open = on_open
293
+ self._user_on_message = on_message
294
+ self._user_on_close = on_close
295
+ self._user_on_error = on_error
296
+
297
+ self._app = WebSocketApp(
298
+ self.ws_url,
299
+ on_open=self._handle_open,
300
+ on_message=self._handle_message,
301
+ on_close=self._handle_close,
302
+ on_error=self._handle_error,
303
+ )
304
+
305
+ def _handle_open(self, ws: WebSocketApp) -> None:
306
+ if callable(self._user_on_open):
307
+ self._user_on_open()
308
+
309
+ def _handle_message(self, ws: WebSocketApp, message: str) -> None:
310
+ try:
311
+ payload = json.loads(message)
312
+ except ValueError:
313
+ payload = {"raw": message}
314
+
315
+ if callable(self._user_on_message):
316
+ self._user_on_message(payload)
317
+
318
+ def _handle_close(self, ws: WebSocketApp, code: int, reason: str) -> None:
319
+ if callable(self._user_on_close):
320
+ self._user_on_close(code, reason or "")
321
+
322
+ def _handle_error(self, ws: WebSocketApp, error: Exception) -> None:
323
+ if callable(self._user_on_error):
324
+ self._user_on_error(error)
325
+
326
+ def send(
327
+ self,
328
+ text: str,
329
+ *,
330
+ event: str = "message",
331
+ meta: Optional[Dict[str, Any]] = None,
332
+ data: Optional[Dict[str, Any]] = None,
333
+ ) -> None:
334
+ if not text or not str(text).strip():
335
+ raise ValueError("text is required.")
336
+
337
+ payload = {
338
+ "event": event,
339
+ "data": {
340
+ "text": str(text).strip(),
341
+ **(data or {}),
342
+ },
343
+ "meta": meta or {},
344
+ }
345
+
346
+ self._app.send(json.dumps(payload))
347
+
348
+ def request_presence_snapshot(self) -> None:
349
+ self._app.send(json.dumps({"action": "presence.snapshot"}))
350
+
351
+ def check_presence(self, identifier_id: str) -> None:
352
+ if not identifier_id or not identifier_id.strip():
353
+ raise ValueError("identifier_id is required.")
354
+
355
+ self._app.send(json.dumps({
356
+ "action": "presence.check",
357
+ "identifier_id": identifier_id.strip(),
358
+ }))
359
+
360
+ def run_forever(self) -> None:
361
+ self._app.run_forever()
362
+
363
+ def close(self) -> None:
364
+ self._app.close()
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: pulsegrid
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the PulseGrid realtime platform
5
+ Author: Kwandile Mofokeng
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Kwandiletshepo/pulsegrid-python
8
+ Project-URL: Repository, https://github.com/Kwandiletshepo/pulsegrid-python
9
+ Project-URL: Issues, https://github.com/Kwandiletshepo/pulsegrid-python/issues
10
+ Keywords: pulsegrid,realtime,websocket,channels,presence,sdk
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: requests>=2.31.0
22
+ Requires-Dist: websocket-client>=1.8.0
23
+
24
+ # PulseGrid Python SDK
25
+
26
+ Official Python SDK for PulseGrid.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install pulsegrid
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ pulsegrid/__init__.py
4
+ pulsegrid/client.py
5
+ pulsegrid.egg-info/PKG-INFO
6
+ pulsegrid.egg-info/SOURCES.txt
7
+ pulsegrid.egg-info/dependency_links.txt
8
+ pulsegrid.egg-info/requires.txt
9
+ pulsegrid.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ requests>=2.31.0
2
+ websocket-client>=1.8.0
@@ -0,0 +1 @@
1
+ pulsegrid
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pulsegrid"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the PulseGrid realtime platform"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Kwandile Mofokeng" }
14
+ ]
15
+ dependencies = [
16
+ "requests>=2.31.0",
17
+ "websocket-client>=1.8.0"
18
+ ]
19
+ keywords = ["pulsegrid", "realtime", "websocket", "channels", "presence", "sdk"]
20
+ classifiers = [
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Operating System :: OS Independent",
27
+ "Intended Audience :: Developers",
28
+ "Topic :: Software Development :: Libraries :: Python Modules"
29
+ ]
30
+
31
+ [tool.setuptools]
32
+ include-package-data = true
33
+
34
+ [tool.setuptools.packages.find]
35
+ include = ["pulsegrid*"]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/Kwandiletshepo/pulsegrid-python"
39
+ Repository = "https://github.com/Kwandiletshepo/pulsegrid-python"
40
+ Issues = "https://github.com/Kwandiletshepo/pulsegrid-python/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+