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.
- wbskt-0.9.0b1/.gitignore +7 -0
- wbskt-0.9.0b1/LICENSE +21 -0
- wbskt-0.9.0b1/PKG-INFO +148 -0
- wbskt-0.9.0b1/README.md +118 -0
- wbskt-0.9.0b1/pyproject.toml +51 -0
- wbskt-0.9.0b1/src/wbskt/__init__.py +25 -0
- wbskt-0.9.0b1/src/wbskt/_http.py +36 -0
- wbskt-0.9.0b1/src/wbskt/_pacing.py +24 -0
- wbskt-0.9.0b1/src/wbskt/client.py +505 -0
- wbskt-0.9.0b1/src/wbskt/models.py +65 -0
- wbskt-0.9.0b1/src/wbskt/protocol.py +38 -0
- wbskt-0.9.0b1/src/wbskt/py.typed +0 -0
- wbskt-0.9.0b1/src/wbskt/reconnect.py +40 -0
- wbskt-0.9.0b1/src/wbskt/storage.py +78 -0
- wbskt-0.9.0b1/src/wbskt/version.py +1 -0
- wbskt-0.9.0b1/tests/conftest.py +144 -0
- wbskt-0.9.0b1/tests/test_client.py +280 -0
- wbskt-0.9.0b1/tests/test_reconnect.py +27 -0
- wbskt-0.9.0b1/tests/test_storage.py +39 -0
- wbskt-0.9.0b1/tests/test_sync_client.py +68 -0
wbskt-0.9.0b1/.gitignore
ADDED
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.
|
wbskt-0.9.0b1/README.md
ADDED
|
@@ -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)
|