wbskt 0.9.0b1__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,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
wbskt-0.9.0b1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WBSKT
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
wbskt-0.9.0b1/PKG-INFO ADDED
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.5
2
+ Name: wbskt
3
+ Version: 0.9.0b1
4
+ Summary: Connect a Raspberry Pi, script or service to Wbskt: register with a policy PIN, hold a WebSocket open, send readings and receive commands.
5
+ Project-URL: Homepage, https://wbskt.com
6
+ Project-URL: Source, https://github.com/wbskt/Wbskt.Python
7
+ Project-URL: Issues, https://github.com/wbskt/Wbskt.Python/issues
8
+ Project-URL: Protocol, https://github.com/wbskt/Wbskt/blob/master/Docs/Client.Protocol.md
9
+ Author: wbskt
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: clients,iot,raspberry-pi,wbskt,websocket
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Home Automation
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: websockets>=13
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
28
+ Requires-Dist: pytest>=8; extra == 'test'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # wbskt
32
+
33
+ Connect a Raspberry Pi, a script or a service to [Wbskt](https://wbskt.com). The client registers
34
+ once with a policy PIN, holds a WebSocket open, sends readings and receives commands. It comes back
35
+ on its own after a drop.
36
+
37
+ ```sh
38
+ pip install wbskt
39
+ ```
40
+
41
+ Python 3.10 or later. The only dependency is `websockets`. This is a 0.9 beta: the client protocol
42
+ it speaks may still change before 1.0.
43
+
44
+ ## Quick start
45
+
46
+ ```python
47
+ from wbskt import WbsktClient
48
+
49
+ client = WbsktClient("greenhouse-sensor", policy_pin="483920")
50
+ client.on_message = lambda message: print("got", message.type, message.payload)
51
+ client.start()
52
+
53
+ client.send("temperature", {"celsius": 21.5})
54
+ ```
55
+
56
+ Get the PIN from a registration policy in the console (**Policies**). The first run registers the
57
+ client and stores its credentials in your user data folder (`~/.local/share/wbskt/<name>.json` on
58
+ Linux), so later runs sign in without registering again. If the policy needs approval, the client
59
+ waits and connects once someone approves it in the console.
60
+
61
+ To keep a script running until Ctrl+C, call `client.run_forever()` instead of `start()`.
62
+
63
+ ## asyncio
64
+
65
+ ```python
66
+ import asyncio
67
+ from wbskt import AsyncWbsktClient
68
+
69
+ async def main():
70
+ client = AsyncWbsktClient("greenhouse-sensor", policy_pin="483920")
71
+
72
+ async def handle(message):
73
+ print(message.type, message.payload)
74
+
75
+ client.on_message = handle
76
+ await client.start()
77
+ while True:
78
+ await client.send("temperature", {"celsius": read_sensor()})
79
+ await asyncio.sleep(10)
80
+
81
+ asyncio.run(main())
82
+ ```
83
+
84
+ Both clients take the same options.
85
+
86
+ ## Commands
87
+
88
+ Tell the console which commands the client takes, and it offers them with a form:
89
+
90
+ ```python
91
+ from wbskt import Command, DataType, Parameter, WbsktClient
92
+
93
+ relay = Command("relay.set", "Switch the relay", [Parameter("on", DataType.BOOLEAN, is_required=True)])
94
+ client = WbsktClient("greenhouse-relay", policy_pin="483920", capabilities=[relay])
95
+
96
+ def handle(message):
97
+ if message.type == "relay.set":
98
+ set_relay(message.payload["on"])
99
+ client.send("relay.state", {"on": message.payload["on"]})
100
+
101
+ client.on_message = handle
102
+ ```
103
+
104
+ The SDK confirms delivery of a command as soon as it arrives, before your handler runs, and refuses
105
+ one that arrives after its expiry without calling your handler. Report the outcome as a message of
106
+ your own if it matters. Call `set_capabilities(...)` when the set of commands changes.
107
+
108
+ ## Version
109
+
110
+ Pass `firmware_version="2.4.1"` to say which version of your own code the client runs. The console
111
+ shows it on the client page. It is not the SDK's version, which is sent for you.
112
+
113
+ ## State
114
+
115
+ ```python
116
+ client.report_state({"humidity": 41, "fan": "on"})
117
+ ```
118
+
119
+ Send only what changed. Numbers are kept as readings and charted in the console.
120
+
121
+ ## While offline
122
+
123
+ `send` never blocks on the network. While disconnected, messages wait in memory (the last 1,000 by
124
+ default, `offline_buffer_size=`) and go out in order, with the time they were made, once the
125
+ connection is back. Workflows can tell those late readings from live ones.
126
+
127
+ ## Options
128
+
129
+ | Option | Default | |
130
+ |---|---|---|
131
+ | `api_url` | `https://api.dev.wbskt.com` | A self-hosted install's `https://api.<domain>`. |
132
+ | `socket_url` | `wss://ws.dev.wbskt.com` | `wss://ws.<domain>`. |
133
+ | `storage` | `FileStorage.for_client(name)` | `FileStorage(path)`, `MemoryStorage(ref, secret)`, or your own object with `load`, `save` and `clear`. |
134
+ | `capabilities` | none | The commands the client takes. |
135
+ | `offline_buffer_size` | 1000 | Messages kept while offline; the oldest go first. |
136
+
137
+ Handlers can be `on_message(message)`, `on_connected()` and `on_disconnected()`. On `WbsktClient`
138
+ they run on the client's background thread, so keep them short.
139
+
140
+ Messages are at most 64 KB, and types starting with `sys.`, `capabilities` and `state.report` are
141
+ reserved; `send` raises `ValueError` for either.
142
+
143
+ ## Protocol
144
+
145
+ The SDK implements [the client protocol](https://github.com/wbskt/Wbskt/blob/master/Docs/Client.Protocol.md)
146
+ and its [WebSocket binding](https://github.com/wbskt/Wbskt/blob/master/Docs/Client.Protocol.WebSocket.md):
147
+ token reuse, jittered reconnects that honour `Retry-After`, ping answers and command
148
+ acknowledgements. Read it to write a client in another language.
@@ -0,0 +1,118 @@
1
+ # wbskt
2
+
3
+ Connect a Raspberry Pi, a script or a service to [Wbskt](https://wbskt.com). The client registers
4
+ once with a policy PIN, holds a WebSocket open, sends readings and receives commands. It comes back
5
+ on its own after a drop.
6
+
7
+ ```sh
8
+ pip install wbskt
9
+ ```
10
+
11
+ Python 3.10 or later. The only dependency is `websockets`. This is a 0.9 beta: the client protocol
12
+ it speaks may still change before 1.0.
13
+
14
+ ## Quick start
15
+
16
+ ```python
17
+ from wbskt import WbsktClient
18
+
19
+ client = WbsktClient("greenhouse-sensor", policy_pin="483920")
20
+ client.on_message = lambda message: print("got", message.type, message.payload)
21
+ client.start()
22
+
23
+ client.send("temperature", {"celsius": 21.5})
24
+ ```
25
+
26
+ Get the PIN from a registration policy in the console (**Policies**). The first run registers the
27
+ client and stores its credentials in your user data folder (`~/.local/share/wbskt/<name>.json` on
28
+ Linux), so later runs sign in without registering again. If the policy needs approval, the client
29
+ waits and connects once someone approves it in the console.
30
+
31
+ To keep a script running until Ctrl+C, call `client.run_forever()` instead of `start()`.
32
+
33
+ ## asyncio
34
+
35
+ ```python
36
+ import asyncio
37
+ from wbskt import AsyncWbsktClient
38
+
39
+ async def main():
40
+ client = AsyncWbsktClient("greenhouse-sensor", policy_pin="483920")
41
+
42
+ async def handle(message):
43
+ print(message.type, message.payload)
44
+
45
+ client.on_message = handle
46
+ await client.start()
47
+ while True:
48
+ await client.send("temperature", {"celsius": read_sensor()})
49
+ await asyncio.sleep(10)
50
+
51
+ asyncio.run(main())
52
+ ```
53
+
54
+ Both clients take the same options.
55
+
56
+ ## Commands
57
+
58
+ Tell the console which commands the client takes, and it offers them with a form:
59
+
60
+ ```python
61
+ from wbskt import Command, DataType, Parameter, WbsktClient
62
+
63
+ relay = Command("relay.set", "Switch the relay", [Parameter("on", DataType.BOOLEAN, is_required=True)])
64
+ client = WbsktClient("greenhouse-relay", policy_pin="483920", capabilities=[relay])
65
+
66
+ def handle(message):
67
+ if message.type == "relay.set":
68
+ set_relay(message.payload["on"])
69
+ client.send("relay.state", {"on": message.payload["on"]})
70
+
71
+ client.on_message = handle
72
+ ```
73
+
74
+ The SDK confirms delivery of a command as soon as it arrives, before your handler runs, and refuses
75
+ one that arrives after its expiry without calling your handler. Report the outcome as a message of
76
+ your own if it matters. Call `set_capabilities(...)` when the set of commands changes.
77
+
78
+ ## Version
79
+
80
+ Pass `firmware_version="2.4.1"` to say which version of your own code the client runs. The console
81
+ shows it on the client page. It is not the SDK's version, which is sent for you.
82
+
83
+ ## State
84
+
85
+ ```python
86
+ client.report_state({"humidity": 41, "fan": "on"})
87
+ ```
88
+
89
+ Send only what changed. Numbers are kept as readings and charted in the console.
90
+
91
+ ## While offline
92
+
93
+ `send` never blocks on the network. While disconnected, messages wait in memory (the last 1,000 by
94
+ default, `offline_buffer_size=`) and go out in order, with the time they were made, once the
95
+ connection is back. Workflows can tell those late readings from live ones.
96
+
97
+ ## Options
98
+
99
+ | Option | Default | |
100
+ |---|---|---|
101
+ | `api_url` | `https://api.dev.wbskt.com` | A self-hosted install's `https://api.<domain>`. |
102
+ | `socket_url` | `wss://ws.dev.wbskt.com` | `wss://ws.<domain>`. |
103
+ | `storage` | `FileStorage.for_client(name)` | `FileStorage(path)`, `MemoryStorage(ref, secret)`, or your own object with `load`, `save` and `clear`. |
104
+ | `capabilities` | none | The commands the client takes. |
105
+ | `offline_buffer_size` | 1000 | Messages kept while offline; the oldest go first. |
106
+
107
+ Handlers can be `on_message(message)`, `on_connected()` and `on_disconnected()`. On `WbsktClient`
108
+ they run on the client's background thread, so keep them short.
109
+
110
+ Messages are at most 64 KB, and types starting with `sys.`, `capabilities` and `state.report` are
111
+ reserved; `send` raises `ValueError` for either.
112
+
113
+ ## Protocol
114
+
115
+ The SDK implements [the client protocol](https://github.com/wbskt/Wbskt/blob/master/Docs/Client.Protocol.md)
116
+ and its [WebSocket binding](https://github.com/wbskt/Wbskt/blob/master/Docs/Client.Protocol.WebSocket.md):
117
+ token reuse, jittered reconnects that honour `Retry-After`, ping answers and command
118
+ acknowledgements. Read it to write a client in another language.
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "wbskt"
7
+ dynamic = ["version"]
8
+ description = "Connect a Raspberry Pi, script or service to Wbskt: register with a policy PIN, hold a WebSocket open, send readings and receive commands."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "wbskt" }]
14
+ keywords = ["wbskt", "iot", "websocket", "raspberry-pi", "clients"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Typing :: Typed",
24
+ "Operating System :: OS Independent",
25
+ "Topic :: Home Automation",
26
+ "Framework :: AsyncIO",
27
+ ]
28
+ dependencies = ["websockets>=13"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://wbskt.com"
32
+ Source = "https://github.com/wbskt/Wbskt.Python"
33
+ Issues = "https://github.com/wbskt/Wbskt.Python/issues"
34
+ Protocol = "https://github.com/wbskt/Wbskt/blob/master/Docs/Client.Protocol.md"
35
+
36
+ [project.optional-dependencies]
37
+ test = ["pytest>=8", "pytest-asyncio>=0.23"]
38
+
39
+ [tool.hatch.version]
40
+ path = "src/wbskt/version.py"
41
+
42
+ [tool.hatch.build.targets.sdist]
43
+ include = ["src/wbskt", "tests", "README.md", "LICENSE"]
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/wbskt"]
47
+
48
+ [tool.pytest.ini_options]
49
+ asyncio_mode = "auto"
50
+ asyncio_default_fixture_loop_scope = "function"
51
+ testpaths = ["tests"]
@@ -0,0 +1,25 @@
1
+ """Connect a client to Wbskt. See README.md, and Docs/Client.Protocol.md for the wire protocol."""
2
+
3
+ from .client import DEFAULT_API_URL, DEFAULT_SOCKET_URL, AsyncWbsktClient, WbsktClient
4
+ from .models import Command, DataType, Message, Parameter, RegistrationRequired, WbsktError
5
+ from .reconnect import ReconnectPolicy
6
+ from .storage import FileStorage, MemoryStorage, Storage
7
+ from .version import __version__
8
+
9
+ __all__ = [
10
+ "AsyncWbsktClient",
11
+ "Command",
12
+ "DataType",
13
+ "DEFAULT_API_URL",
14
+ "DEFAULT_SOCKET_URL",
15
+ "FileStorage",
16
+ "MemoryStorage",
17
+ "Message",
18
+ "Parameter",
19
+ "ReconnectPolicy",
20
+ "RegistrationRequired",
21
+ "Storage",
22
+ "WbsktClient",
23
+ "WbsktError",
24
+ "__version__",
25
+ ]
@@ -0,0 +1,36 @@
1
+ """The two HTTPS calls a client makes, on the standard library so the SDK has one dependency."""
2
+
3
+ import json
4
+ import urllib.error
5
+ import urllib.request
6
+ from typing import Any, Dict, Mapping, Optional, Tuple
7
+
8
+ Response = Tuple[int, Any, Mapping[str, str]]
9
+
10
+ TIMEOUT = 30.0
11
+
12
+
13
+ def post_json(url: str, body: Dict[str, Any], timeout: float = TIMEOUT) -> Response:
14
+ """POST ``body`` as JSON. Returns the status, the parsed body (None if it isn't JSON) and the headers.
15
+
16
+ An HTTP error status is returned, not raised; a network failure raises ``OSError``.
17
+ """
18
+ request = urllib.request.Request(
19
+ url,
20
+ data=json.dumps(body).encode("utf-8"),
21
+ headers={"Content-Type": "application/json", "Accept": "application/json"},
22
+ method="POST",
23
+ )
24
+ try:
25
+ with urllib.request.urlopen(request, timeout=timeout) as response:
26
+ return response.status, _parse(response.read()), dict(response.headers)
27
+ except urllib.error.HTTPError as error:
28
+ with error:
29
+ return error.code, _parse(error.read()), dict(error.headers or {})
30
+
31
+
32
+ def _parse(raw: bytes) -> Optional[Any]:
33
+ try:
34
+ return json.loads(raw) if raw else None
35
+ except ValueError:
36
+ return None
@@ -0,0 +1,24 @@
1
+ """A token bucket that paces the client's sends below the gateway's message budget."""
2
+
3
+ import asyncio
4
+ import time
5
+
6
+
7
+ class Pacer:
8
+ """``burst`` sends back to back, then ``per_second``. Single event loop only."""
9
+
10
+ def __init__(self, per_second: float, burst: int) -> None:
11
+ self._per_second = max(0.1, per_second)
12
+ self._burst = max(1, burst)
13
+ self._tokens = float(self._burst)
14
+ self._last = time.monotonic()
15
+
16
+ async def take(self) -> None:
17
+ while True:
18
+ now = time.monotonic()
19
+ self._tokens = min(self._burst, self._tokens + (now - self._last) * self._per_second)
20
+ self._last = now
21
+ if self._tokens >= 1:
22
+ self._tokens -= 1
23
+ return
24
+ await asyncio.sleep((1 - self._tokens) / self._per_second)