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.
- pulsegrid-0.1.0/PKG-INFO +31 -0
- pulsegrid-0.1.0/README.md +8 -0
- pulsegrid-0.1.0/pulsegrid/__init__.py +4 -0
- pulsegrid-0.1.0/pulsegrid/client.py +364 -0
- pulsegrid-0.1.0/pulsegrid.egg-info/PKG-INFO +31 -0
- pulsegrid-0.1.0/pulsegrid.egg-info/SOURCES.txt +9 -0
- pulsegrid-0.1.0/pulsegrid.egg-info/dependency_links.txt +1 -0
- pulsegrid-0.1.0/pulsegrid.egg-info/requires.txt +2 -0
- pulsegrid-0.1.0/pulsegrid.egg-info/top_level.txt +1 -0
- pulsegrid-0.1.0/pyproject.toml +40 -0
- pulsegrid-0.1.0/setup.cfg +4 -0
pulsegrid-0.1.0/PKG-INFO
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -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"
|