python-jebao 0.1.0__py3-none-any.whl
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.
- jebao/__init__.py +35 -0
- jebao/const.py +68 -0
- jebao/device.py +139 -0
- jebao/discovery.py +399 -0
- jebao/exceptions.py +25 -0
- jebao/mdp20000.py +302 -0
- jebao/protocol.py +670 -0
- jebao/retry.py +77 -0
- python_jebao-0.1.0.dist-info/METADATA +367 -0
- python_jebao-0.1.0.dist-info/RECORD +13 -0
- python_jebao-0.1.0.dist-info/WHEEL +5 -0
- python_jebao-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_jebao-0.1.0.dist-info/top_level.txt +1 -0
jebao/retry.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Retry logic for handling transient failures."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Callable, Type, Tuple
|
|
6
|
+
|
|
7
|
+
from .exceptions import JebaoConnectionError, JebaoTimeoutError
|
|
8
|
+
|
|
9
|
+
_LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def async_retry(
|
|
13
|
+
max_attempts: int = 3,
|
|
14
|
+
delay: float = 0.5,
|
|
15
|
+
backoff: float = 2.0,
|
|
16
|
+
exceptions: Tuple[Type[Exception], ...] = (JebaoConnectionError, JebaoTimeoutError),
|
|
17
|
+
):
|
|
18
|
+
"""Decorator to retry async functions on specific exceptions.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
max_attempts: Maximum number of attempts (default 3)
|
|
22
|
+
delay: Initial delay between retries in seconds (default 0.5)
|
|
23
|
+
backoff: Multiplier for delay on each retry (default 2.0)
|
|
24
|
+
exceptions: Tuple of exception types to retry on
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
@async_retry(max_attempts=3, delay=1.0)
|
|
28
|
+
async def send_command(self):
|
|
29
|
+
# Code that might fail due to garbage bytes
|
|
30
|
+
pass
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def decorator(func: Callable):
|
|
34
|
+
@functools.wraps(func)
|
|
35
|
+
async def wrapper(*args, **kwargs):
|
|
36
|
+
attempt = 1
|
|
37
|
+
current_delay = delay
|
|
38
|
+
|
|
39
|
+
while attempt <= max_attempts:
|
|
40
|
+
try:
|
|
41
|
+
return await func(*args, **kwargs)
|
|
42
|
+
except exceptions as e:
|
|
43
|
+
if attempt == max_attempts:
|
|
44
|
+
_LOGGER.error(
|
|
45
|
+
f"{func.__name__} failed after {max_attempts} attempts: {e}"
|
|
46
|
+
)
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
# Check if error message indicates garbage issue
|
|
50
|
+
error_msg = str(e).lower()
|
|
51
|
+
is_garbage_issue = (
|
|
52
|
+
"synchronization" in error_msg
|
|
53
|
+
or "garbage" in error_msg
|
|
54
|
+
or "invalid header" in error_msg
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if is_garbage_issue:
|
|
58
|
+
_LOGGER.warning(
|
|
59
|
+
f"{func.__name__} attempt {attempt}/{max_attempts} failed "
|
|
60
|
+
f"due to garbage bytes, retrying in {current_delay}s: {e}"
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
_LOGGER.warning(
|
|
64
|
+
f"{func.__name__} attempt {attempt}/{max_attempts} failed, "
|
|
65
|
+
f"retrying in {current_delay}s: {e}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
await asyncio.sleep(current_delay)
|
|
69
|
+
attempt += 1
|
|
70
|
+
current_delay *= backoff
|
|
71
|
+
|
|
72
|
+
# This should never be reached due to raise in loop
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
return wrapper
|
|
76
|
+
|
|
77
|
+
return decorator
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-jebao
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python library for controlling Jebao aquarium pumps
|
|
5
|
+
Home-page: https://github.com/jrigling/python-jebao
|
|
6
|
+
Author: Justin Rigling
|
|
7
|
+
Author-email: Justin Rigling <jrigling@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/jrigling/python-jebao
|
|
10
|
+
Project-URL: Repository, https://github.com/jrigling/python-jebao
|
|
11
|
+
Project-URL: Issues, https://github.com/jrigling/python-jebao/issues
|
|
12
|
+
Project-URL: Documentation, https://github.com/jrigling/python-jebao#readme
|
|
13
|
+
Keywords: jebao,aquarium,pump,home-assistant,iot
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Home Automation
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: netifaces>=0.11.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
33
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
35
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
36
|
+
Provides-Extra: build
|
|
37
|
+
Requires-Dist: build>=0.10.0; extra == "build"
|
|
38
|
+
Requires-Dist: twine>=4.0.0; extra == "build"
|
|
39
|
+
Dynamic: author
|
|
40
|
+
Dynamic: home-page
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
|
|
44
|
+
# python-jebao
|
|
45
|
+
|
|
46
|
+
Python library for controlling Jebao aquarium pumps over local network.
|
|
47
|
+
|
|
48
|
+
## Supported Models
|
|
49
|
+
|
|
50
|
+
- **MDP-20000**: Variable-speed circulation pump (30-100% speed control)
|
|
51
|
+
- **MD-4.4**: 4-channel dosing pump (planned)
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
- 🔍 **UDP Discovery** - Automatic device discovery with multi-subnet support
|
|
56
|
+
- 🔌 **Local Control** - Direct TCP control, no cloud dependency
|
|
57
|
+
- ⚡ **Full Protocol** - On/Off, speed control, feed mode, status monitoring
|
|
58
|
+
- 🏠 **Home Assistant Ready** - Designed for HA integration
|
|
59
|
+
- 🔒 **Type Safe** - Full type hints for better IDE support
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install python-jebao
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For development:
|
|
68
|
+
```bash
|
|
69
|
+
git clone https://github.com/jrigling/python-jebao.git
|
|
70
|
+
cd python-jebao
|
|
71
|
+
pip install -e .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Quick Start
|
|
75
|
+
|
|
76
|
+
### Discovery
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import asyncio
|
|
80
|
+
from jebao import discover_devices
|
|
81
|
+
|
|
82
|
+
async def main():
|
|
83
|
+
# Discover all Jebao devices on network
|
|
84
|
+
devices = await discover_devices()
|
|
85
|
+
|
|
86
|
+
for device in devices:
|
|
87
|
+
print(f"Found: {device.model} at {device.ip_address}")
|
|
88
|
+
print(f" Device ID: {device.device_id}")
|
|
89
|
+
print(f" MAC: {device.mac_address}")
|
|
90
|
+
|
|
91
|
+
asyncio.run(main())
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Multi-Subnet Discovery
|
|
95
|
+
|
|
96
|
+
If you have multiple network interfaces (e.g., IoT VLAN, main network):
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# Discover on all interfaces
|
|
100
|
+
devices = await discover_devices()
|
|
101
|
+
|
|
102
|
+
# Or specify interfaces explicitly
|
|
103
|
+
devices = await discover_devices(interfaces=['eth0', 'eth1'])
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Basic Control
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from jebao import MDP20000Device
|
|
110
|
+
|
|
111
|
+
async def main():
|
|
112
|
+
# Connect to device
|
|
113
|
+
async with MDP20000Device(host="10.20.20.13") as pump:
|
|
114
|
+
# Ensure in manual mode (exits Program mode if needed)
|
|
115
|
+
await pump.ensure_manual_mode()
|
|
116
|
+
|
|
117
|
+
# Turn on
|
|
118
|
+
await pump.turn_on()
|
|
119
|
+
|
|
120
|
+
# Set speed to 75%
|
|
121
|
+
await pump.set_speed(75)
|
|
122
|
+
|
|
123
|
+
# Check status
|
|
124
|
+
await pump.update()
|
|
125
|
+
print(f"State: {pump.state.name}")
|
|
126
|
+
print(f"Speed: {pump.speed}%")
|
|
127
|
+
print(f"Is on: {pump.is_on}")
|
|
128
|
+
|
|
129
|
+
# Turn off
|
|
130
|
+
await pump.turn_off()
|
|
131
|
+
|
|
132
|
+
asyncio.run(main())
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Feed Mode
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
async def feed_mode_example():
|
|
139
|
+
async with MDP20000Device(host="10.20.20.13") as pump:
|
|
140
|
+
await pump.ensure_manual_mode()
|
|
141
|
+
await pump.turn_on()
|
|
142
|
+
await pump.set_speed(75)
|
|
143
|
+
|
|
144
|
+
# Start 2-minute feed (pump stops temporarily)
|
|
145
|
+
await pump.start_feed(minutes=2)
|
|
146
|
+
print("Feed started, pump will auto-resume in 2 minutes")
|
|
147
|
+
|
|
148
|
+
# Check status during feed
|
|
149
|
+
await pump.update()
|
|
150
|
+
print(f"In feed mode: {pump.is_feed_mode}")
|
|
151
|
+
|
|
152
|
+
# Or cancel early
|
|
153
|
+
# await pump.cancel_feed(resume_speed=75)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Long-Running Connection
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
async def monitor_pump():
|
|
160
|
+
pump = MDP20000Device(host="10.20.20.13")
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
await pump.connect()
|
|
164
|
+
await pump.ensure_manual_mode()
|
|
165
|
+
|
|
166
|
+
# Control pump
|
|
167
|
+
await pump.turn_on()
|
|
168
|
+
await pump.set_speed(50)
|
|
169
|
+
|
|
170
|
+
# Monitor status periodically
|
|
171
|
+
while True:
|
|
172
|
+
await pump.update()
|
|
173
|
+
print(f"Status: {pump.state.name} @ {pump.speed}%")
|
|
174
|
+
await asyncio.sleep(30)
|
|
175
|
+
|
|
176
|
+
finally:
|
|
177
|
+
await pump.disconnect()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Device States
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from jebao import DeviceState
|
|
184
|
+
|
|
185
|
+
# State values
|
|
186
|
+
DeviceState.OFF # 0x10 - Manual mode, stopped
|
|
187
|
+
DeviceState.ON # 0x11 - Manual mode, running
|
|
188
|
+
DeviceState.FEED # 0x15 - Feed mode (temporary pause)
|
|
189
|
+
DeviceState.PROGRAM # 0x19 - Program mode (scheduled)
|
|
190
|
+
|
|
191
|
+
# Check state
|
|
192
|
+
if pump.state == DeviceState.FEED:
|
|
193
|
+
print("Pump is in feed mode")
|
|
194
|
+
|
|
195
|
+
# Helper properties
|
|
196
|
+
pump.is_on # True if ON or FEED
|
|
197
|
+
pump.is_off # True if OFF
|
|
198
|
+
pump.is_feed_mode # True if FEED
|
|
199
|
+
pump.is_program_mode # True if PROGRAM
|
|
200
|
+
pump.is_manual_mode # True if ON or OFF
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## API Reference
|
|
204
|
+
|
|
205
|
+
### MDP20000Device
|
|
206
|
+
|
|
207
|
+
#### Connection
|
|
208
|
+
- `async connect(timeout=5.0)` - Connect and authenticate
|
|
209
|
+
- `async disconnect()` - Disconnect from device
|
|
210
|
+
- `is_connected` - Check connection status
|
|
211
|
+
- `async update()` - Refresh device status
|
|
212
|
+
|
|
213
|
+
#### Control
|
|
214
|
+
- `async turn_on()` - Turn pump on
|
|
215
|
+
- `async turn_off()` - Turn pump off
|
|
216
|
+
- `async set_speed(percentage)` - Set speed (30-100%)
|
|
217
|
+
|
|
218
|
+
#### Feed Mode
|
|
219
|
+
- `async set_feed_duration(minutes)` - Configure feed timer (1-10 minutes)
|
|
220
|
+
- `async start_feed(minutes=None)` - Start feed mode
|
|
221
|
+
- `async cancel_feed(resume_speed=None)` - Cancel feed and resume
|
|
222
|
+
|
|
223
|
+
#### Program Mode
|
|
224
|
+
- `async ensure_manual_mode()` - Exit Program mode if active
|
|
225
|
+
|
|
226
|
+
#### Properties
|
|
227
|
+
- `state: DeviceState` - Current state
|
|
228
|
+
- `speed: int` - Current speed (30-100)
|
|
229
|
+
- `is_on: bool` - Pump is running
|
|
230
|
+
- `is_off: bool` - Pump is stopped
|
|
231
|
+
- `is_feed_mode: bool` - In feed mode
|
|
232
|
+
- `is_program_mode: bool` - In program mode
|
|
233
|
+
- `is_manual_mode: bool` - In manual mode
|
|
234
|
+
|
|
235
|
+
### Discovery
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from jebao import JebaoDiscovery, discover_devices
|
|
239
|
+
|
|
240
|
+
# Convenience function
|
|
241
|
+
devices = await discover_devices(timeout=2.0, interfaces=['eth0'])
|
|
242
|
+
|
|
243
|
+
# Or use class directly
|
|
244
|
+
discovery = JebaoDiscovery()
|
|
245
|
+
devices = await discovery.discover(timeout=2.0, interfaces=['eth0'])
|
|
246
|
+
|
|
247
|
+
# Filter by model
|
|
248
|
+
mdp20000_devices = [d for d in devices if d.is_mdp20000]
|
|
249
|
+
md44_devices = [d for d in devices if d.is_md44]
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### DiscoveredDevice
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
device.device_id # Device ID (e.g., "POAKSFXJNJ")
|
|
256
|
+
device.ip_address # IP address
|
|
257
|
+
device.mac_address # MAC address
|
|
258
|
+
device.product_key # Product key
|
|
259
|
+
device.model # Model name (e.g., "MDP-20000")
|
|
260
|
+
device.is_mdp20000 # True if MDP-20000
|
|
261
|
+
device.is_md44 # True if MD-4.4
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Home Assistant Integration
|
|
265
|
+
|
|
266
|
+
This library is designed for use with Home Assistant. See the separate
|
|
267
|
+
[homeassistant-jebao](https://github.com/jrigling/homeassistant-jebao) repository
|
|
268
|
+
for the ready-to-use HA integration.
|
|
269
|
+
|
|
270
|
+
## Reliability Features
|
|
271
|
+
|
|
272
|
+
The library includes automatic retry logic for enhanced reliability:
|
|
273
|
+
- Commands automatically retry up to 3 times on transient failures
|
|
274
|
+
- Handles garbage byte accumulation from pump firmware
|
|
275
|
+
- Exponential backoff for retry delays
|
|
276
|
+
- Typical success rate >97% even with flaky network conditions
|
|
277
|
+
|
|
278
|
+
## Multi-Subnet Considerations
|
|
279
|
+
|
|
280
|
+
When Home Assistant runs on a system with multiple network interfaces:
|
|
281
|
+
|
|
282
|
+
1. **Automatic Discovery**: The library will scan all interfaces by default
|
|
283
|
+
2. **Interface Selection**: You can specify which interfaces to scan
|
|
284
|
+
3. **Broadcast Support**: Each interface gets its own broadcast address
|
|
285
|
+
4. **IoT VLANs**: Works great with isolated IoT networks
|
|
286
|
+
|
|
287
|
+
Example: HA connected to both `192.168.1.0/24` (main) and `10.20.20.0/24` (IoT):
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
# Discovers on both networks automatically
|
|
291
|
+
devices = await discover_devices()
|
|
292
|
+
|
|
293
|
+
# Or be explicit
|
|
294
|
+
devices = await discover_devices(interfaces=['eth0', 'eth1'])
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Error Handling
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
from jebao import (
|
|
301
|
+
JebaoError,
|
|
302
|
+
JebaoConnectionError,
|
|
303
|
+
JebaoAuthenticationError,
|
|
304
|
+
JebaoCommandError,
|
|
305
|
+
JebaoTimeoutError,
|
|
306
|
+
JebaoInvalidStateError,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
async with MDP20000Device(host="10.20.20.13") as pump:
|
|
311
|
+
await pump.turn_on()
|
|
312
|
+
except JebaoConnectionError:
|
|
313
|
+
print("Failed to connect - check IP and network")
|
|
314
|
+
except JebaoAuthenticationError:
|
|
315
|
+
print("Authentication failed")
|
|
316
|
+
except JebaoCommandError:
|
|
317
|
+
print("Command execution failed")
|
|
318
|
+
except JebaoTimeoutError:
|
|
319
|
+
print("Operation timed out")
|
|
320
|
+
except JebaoError as e:
|
|
321
|
+
print(f"General error: {e}")
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Logging
|
|
325
|
+
|
|
326
|
+
The library uses Python's standard logging:
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
import logging
|
|
330
|
+
|
|
331
|
+
# Enable debug logging
|
|
332
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
333
|
+
|
|
334
|
+
# Or just for jebao
|
|
335
|
+
logging.getLogger('jebao').setLevel(logging.DEBUG)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Requirements
|
|
339
|
+
|
|
340
|
+
- Python 3.9+
|
|
341
|
+
- `netifaces` - For multi-interface network discovery
|
|
342
|
+
|
|
343
|
+
## License
|
|
344
|
+
|
|
345
|
+
MIT License - See LICENSE file
|
|
346
|
+
|
|
347
|
+
## Contributing
|
|
348
|
+
|
|
349
|
+
Contributions welcome! Please:
|
|
350
|
+
|
|
351
|
+
1. Fork the repository
|
|
352
|
+
2. Create a feature branch
|
|
353
|
+
3. Add tests for new functionality
|
|
354
|
+
4. Ensure all tests pass
|
|
355
|
+
5. Submit a pull request
|
|
356
|
+
|
|
357
|
+
## Credits
|
|
358
|
+
|
|
359
|
+
This library builds upon the excellent work from:
|
|
360
|
+
- [jebao-dosing-pump-md-4.4](https://github.com/tancou/jebao-dosing-pump-md-4.4) by [@tancou](https://github.com/tancou) - Original Node.js implementation for MD 4.4 pumps that provided the foundation for understanding the GizWits protocol
|
|
361
|
+
|
|
362
|
+
Protocol for MDP-20000 was reverse-engineered through packet capture analysis of the official Jebao mobile app.
|
|
363
|
+
|
|
364
|
+
## Disclaimer
|
|
365
|
+
|
|
366
|
+
This is an unofficial library not affiliated with Jebao. Use at your own risk.
|
|
367
|
+
Device warranty may be affected by third-party control software.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
jebao/__init__.py,sha256=0FRNVM0yhzfm1vJthqhe7TgFSfyRtHn_ao74LbkDXO4,761
|
|
2
|
+
jebao/const.py,sha256=KasF1RntYAwC_2Gv4wmF4VE4OQgTzQ5UAYugdvk8oBg,1621
|
|
3
|
+
jebao/device.py,sha256=1NF-EG6H5_DtavRdXdwdPFxHKBAU7zdrwBsPxMCfAL0,4023
|
|
4
|
+
jebao/discovery.py,sha256=nyXhu_BPXKzjF5lBeK2W62R_RtVhNnpcuulVvH20X9k,13365
|
|
5
|
+
jebao/exceptions.py,sha256=S8jO0kxaw9l_TMdITgZm2gx-olJdGB3746tLB8UahJ0,501
|
|
6
|
+
jebao/mdp20000.py,sha256=eyEDTpNm3DJ1uEIaqi2MLou1I63X9-GQzJXo-9xggok,9559
|
|
7
|
+
jebao/protocol.py,sha256=r2tZX_qnxJRY957qWXApicRDSXNW78HholkEsk4Opxc,25044
|
|
8
|
+
jebao/retry.py,sha256=N9biedjTigFRVmVrEAE_iPmuTW2GZJrv8B_zMQ8hC_A,2646
|
|
9
|
+
python_jebao-0.1.0.dist-info/licenses/LICENSE,sha256=xz5RpUswwNDP6Agh5an2XKA3BJfABxtPaeqPTDn1y2k,1071
|
|
10
|
+
python_jebao-0.1.0.dist-info/METADATA,sha256=btSVlVg--7HRXJrK2_RCA3clTTXYtbrkv0qWRejCEwI,10143
|
|
11
|
+
python_jebao-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
python_jebao-0.1.0.dist-info/top_level.txt,sha256=Bj9cYMUNXSj-Q_ViTIEIYJyfKOmnoywBAspe6ZprUkY,6
|
|
13
|
+
python_jebao-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Justin Rigling
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jebao
|