timberborn-http 0.0.2__tar.gz → 0.0.4__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.
- {timberborn_http-0.0.2/src/timberborn_http.egg-info → timberborn_http-0.0.4}/PKG-INFO +12 -24
- timberborn_http-0.0.4/README.md +57 -0
- {timberborn_http-0.0.2 → timberborn_http-0.0.4}/pyproject.toml +1 -1
- timberborn_http-0.0.4/src/timberborn_http/__init__.py +14 -0
- timberborn_http-0.0.4/src/timberborn_http/_client.py +325 -0
- {timberborn_http-0.0.2 → timberborn_http-0.0.4/src/timberborn_http.egg-info}/PKG-INFO +12 -24
- timberborn_http-0.0.2/README.md +0 -69
- timberborn_http-0.0.2/src/timberborn_http/__init__.py +0 -2
- timberborn_http-0.0.2/src/timberborn_http/_client.py +0 -417
- {timberborn_http-0.0.2 → timberborn_http-0.0.4}/LICENSE +0 -0
- {timberborn_http-0.0.2 → timberborn_http-0.0.4}/setup.cfg +0 -0
- {timberborn_http-0.0.2 → timberborn_http-0.0.4}/src/timberborn_http.egg-info/SOURCES.txt +0 -0
- {timberborn_http-0.0.2 → timberborn_http-0.0.4}/src/timberborn_http.egg-info/dependency_links.txt +0 -0
- {timberborn_http-0.0.2 → timberborn_http-0.0.4}/src/timberborn_http.egg-info/requires.txt +0 -0
- {timberborn_http-0.0.2 → timberborn_http-0.0.4}/src/timberborn_http.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: timberborn_http
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Timberborn http api wrapper
|
|
5
5
|
Author-email: Agroqirax <124404386+Agroqirax@users.noreply.github.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -22,10 +22,9 @@ Supports both direct API control and webhook-based event handling.
|
|
|
22
22
|
|
|
23
23
|
## Features
|
|
24
24
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
- **Multiple interaction styles**: OOP, decorators, and direct function calls
|
|
25
|
+
- Get the state of levers & adapters from timberborn
|
|
26
|
+
- Update the state of levers
|
|
27
|
+
- Receive webhooks and trigger actions
|
|
29
28
|
|
|
30
29
|
## Installation
|
|
31
30
|
|
|
@@ -48,8 +47,8 @@ for lever in levers:
|
|
|
48
47
|
print(lever.name, lever.state)
|
|
49
48
|
|
|
50
49
|
# Control a lever
|
|
51
|
-
api.switch_on("
|
|
52
|
-
api.set_color("
|
|
50
|
+
api.switch_on("HTTP Lever 1")
|
|
51
|
+
api.set_color("HTTP Lever 1", "ff0000")
|
|
53
52
|
```
|
|
54
53
|
|
|
55
54
|
### 2. Webhook Events
|
|
@@ -57,30 +56,19 @@ api.set_color("Main Water Pump", "ff0000")
|
|
|
57
56
|
```python
|
|
58
57
|
from timberborn_http import TimberbornWebhookServer
|
|
59
58
|
|
|
60
|
-
server = TimberbornWebhookServer()
|
|
59
|
+
server = TimberbornWebhookServer(port=8081)
|
|
61
60
|
|
|
62
|
-
@server.
|
|
61
|
+
@server.on_event("HTTP Lever 1")
|
|
63
62
|
def handle_on(name):
|
|
64
63
|
print(f"{name} turned ON!")
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
while True:
|
|
66
|
+
pass
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
- [API Usage](/examples/api): Direct control and polling
|
|
72
|
-
- [Webhook Usage](/examples/webhooks): Real-time event handling
|
|
69
|
+
More examples & details in the examples folder
|
|
73
70
|
|
|
74
71
|
## Getting Help
|
|
75
72
|
|
|
76
73
|
- [GitHub Issues](https://github.com/agroqirax/timberborn-http/issues)
|
|
77
|
-
- [Discord](https://discord.gg/timberborn)
|
|
78
|
-
|
|
79
|
-
## Commands
|
|
80
|
-
|
|
81
|
-
- `python -m venv .venv`
|
|
82
|
-
- `pip install -r requirements.txt`
|
|
83
|
-
- `pip install -e .`
|
|
84
|
-
- `python -m build`
|
|
85
|
-
- `python3 -m twine upload --repository testpypi dist/*`
|
|
86
|
-
- `python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps timberborn_http`
|
|
74
|
+
- [Discord](https://discord.gg/timberborn) in `#⏱️automation` or `#🤖mod-creators`
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Timberborn HTTP API Wrapper
|
|
2
|
+
|
|
3
|
+
A Python library for interacting with the Timberborn HTTP automation API.
|
|
4
|
+
Supports both direct API control and webhook-based event handling.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- Get the state of levers & adapters from timberborn
|
|
9
|
+
- Update the state of levers
|
|
10
|
+
- Receive webhooks and trigger actions
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install timberborn-http
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
### 1. Direct API Control
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from timberborn_http import TimberbornAPI
|
|
24
|
+
|
|
25
|
+
api = TimberbornAPI("http://localhost:8080")
|
|
26
|
+
|
|
27
|
+
# Get all levers
|
|
28
|
+
levers = api.get_levers()
|
|
29
|
+
for lever in levers:
|
|
30
|
+
print(lever.name, lever.state)
|
|
31
|
+
|
|
32
|
+
# Control a lever
|
|
33
|
+
api.switch_on("HTTP Lever 1")
|
|
34
|
+
api.set_color("HTTP Lever 1", "ff0000")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Webhook Events
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from timberborn_http import TimberbornWebhookServer
|
|
41
|
+
|
|
42
|
+
server = TimberbornWebhookServer(port=8081)
|
|
43
|
+
|
|
44
|
+
@server.on_event("HTTP Lever 1")
|
|
45
|
+
def handle_on(name):
|
|
46
|
+
print(f"{name} turned ON!")
|
|
47
|
+
|
|
48
|
+
while True:
|
|
49
|
+
pass
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
More examples & details in the examples folder
|
|
53
|
+
|
|
54
|
+
## Getting Help
|
|
55
|
+
|
|
56
|
+
- [GitHub Issues](https://github.com/agroqirax/timberborn-http/issues)
|
|
57
|
+
- [Discord](https://discord.gg/timberborn) in `#⏱️automation` or `#🤖mod-creators`
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Timberborn HTTP
|
|
3
|
+
Basic usage:
|
|
4
|
+
```
|
|
5
|
+
from timberborn_http import TimberbornAPI
|
|
6
|
+
api = TimberbornAPI("http://localhost:8080"
|
|
7
|
+
api.get_levers()
|
|
8
|
+
```
|
|
9
|
+
:copyright: (c) 2026 by Agroqirax
|
|
10
|
+
:license: GNU GPL-3.0, see LICENSE for more details.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from ._client import TimberbornAPI, TimberbornWebhookServer
|
|
14
|
+
__all__ = ["TimberbornAPI", "TimberbornWebhookServer"]
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
from typing import Callable, Dict, List, Optional, overload
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import urllib.parse
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from flask import Flask, cli
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("timberborn_http")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TimberbornAPI:
|
|
13
|
+
"""
|
|
14
|
+
Client for interacting with the Timberborn HTTP automation API.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, host: str = "http://localhost:8080"):
|
|
18
|
+
self.host = host.rstrip("/")
|
|
19
|
+
|
|
20
|
+
def _encode(self, name: str) -> str:
|
|
21
|
+
return urllib.parse.quote(name, safe="")
|
|
22
|
+
|
|
23
|
+
def _request(self, path: str) -> requests.Response:
|
|
24
|
+
url = f"{self.host}{path}"
|
|
25
|
+
try:
|
|
26
|
+
r = requests.get(url, timeout=5)
|
|
27
|
+
r.raise_for_status()
|
|
28
|
+
return r
|
|
29
|
+
except requests.exceptions.ConnectionError as e:
|
|
30
|
+
raise ConnectionError(
|
|
31
|
+
f"Failed to connect to Timberborn API at {self.host}. "
|
|
32
|
+
f"Is Timberborn running & the API started?"
|
|
33
|
+
) from e
|
|
34
|
+
except requests.exceptions.Timeout as e:
|
|
35
|
+
raise TimeoutError(
|
|
36
|
+
f"Request to {url} timed out."
|
|
37
|
+
) from e
|
|
38
|
+
except requests.exceptions.HTTPError as e:
|
|
39
|
+
raise RuntimeError(
|
|
40
|
+
f"HTTP error from Timberborn API: {r.status_code} {r.reason} — {url}"
|
|
41
|
+
) from e
|
|
42
|
+
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
# Fetchers
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
def get_adapters(self) -> List["TimberbornAPI.Adapter"]:
|
|
48
|
+
"""Retrieve all adapters."""
|
|
49
|
+
logger.debug("Fetching all adapters")
|
|
50
|
+
data = self._request("/api/adapters").json()
|
|
51
|
+
return [self.Adapter(self, d["name"]) for d in data]
|
|
52
|
+
|
|
53
|
+
def get_levers(self) -> List["TimberbornAPI.Lever"]:
|
|
54
|
+
"""Retrieve all levers."""
|
|
55
|
+
logger.debug("Fetching all levers")
|
|
56
|
+
data = self._request("/api/levers").json()
|
|
57
|
+
return [self.Lever(self, d["name"]) for d in data]
|
|
58
|
+
|
|
59
|
+
def get_adapter(self, name: str) -> "TimberbornAPI.Adapter":
|
|
60
|
+
"""Retrieve a single adapter by name."""
|
|
61
|
+
logger.debug("Fetching adapter '%s'", name)
|
|
62
|
+
# validates existence
|
|
63
|
+
self._request(f"/api/adapters/{self._encode(name)}")
|
|
64
|
+
return self.Adapter(self, name)
|
|
65
|
+
|
|
66
|
+
def get_lever(self, name: str) -> "TimberbornAPI.Lever":
|
|
67
|
+
"""Retrieve a single lever by name."""
|
|
68
|
+
logger.debug("Fetching lever '%s'", name)
|
|
69
|
+
# validates existence
|
|
70
|
+
self._request(f"/api/levers/{self._encode(name)}")
|
|
71
|
+
return self.Lever(self, name)
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
# Low-level control (used by Adapter/Lever, but also callable directly)
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def get_adapter_state(self, name: str) -> bool:
|
|
78
|
+
"""Fetch the live state of an adapter."""
|
|
79
|
+
data = self._request(f"/api/adapters/{self._encode(name)}").json()
|
|
80
|
+
return bool(data["state"])
|
|
81
|
+
|
|
82
|
+
def get_lever_state(self, name: str) -> bool:
|
|
83
|
+
"""Fetch the live state of a lever."""
|
|
84
|
+
data = self._request(f"/api/levers/{self._encode(name)}").json()
|
|
85
|
+
return bool(data["state"])
|
|
86
|
+
|
|
87
|
+
def get_lever_spring_return(self, name: str) -> bool:
|
|
88
|
+
"""Fetch the live springReturn value of a lever."""
|
|
89
|
+
data = self._request(f"/api/levers/{self._encode(name)}").json()
|
|
90
|
+
return bool(data["springReturn"])
|
|
91
|
+
|
|
92
|
+
def switch_on(self, name: str) -> None:
|
|
93
|
+
"""Switch a lever ON."""
|
|
94
|
+
logger.info("Switching ON lever '%s'", name)
|
|
95
|
+
self._request(f"/api/switch-on/{self._encode(name)}")
|
|
96
|
+
|
|
97
|
+
def switch_off(self, name: str) -> None:
|
|
98
|
+
"""Switch a lever OFF."""
|
|
99
|
+
logger.info("Switching OFF lever '%s'", name)
|
|
100
|
+
self._request(f"/api/switch-off/{self._encode(name)}")
|
|
101
|
+
|
|
102
|
+
def toggle(self, name: str) -> None:
|
|
103
|
+
"""Toggle a lever between ON and OFF."""
|
|
104
|
+
logger.info("Toggling lever '%s'", name)
|
|
105
|
+
if self.get_lever_state(name):
|
|
106
|
+
self.switch_off(name)
|
|
107
|
+
else:
|
|
108
|
+
self.switch_on(name)
|
|
109
|
+
|
|
110
|
+
def set_color(self, name: str, hex_color: str) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Set lever color.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
hex_color : str
|
|
117
|
+
Hex color without '#' (e.g. '00ff00').
|
|
118
|
+
"""
|
|
119
|
+
hex_color = hex_color.lstrip("#")
|
|
120
|
+
if len(hex_color) != 6 or not all(c in "0123456789abcdefABCDEF" for c in hex_color):
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"Invalid hex color: '{hex_color}'. Expected 6 hex digits.")
|
|
123
|
+
logger.info("Setting color of '%s' to #%s", name, hex_color)
|
|
124
|
+
self._request(f"/api/color/{self._encode(name)}/{hex_color}")
|
|
125
|
+
|
|
126
|
+
# ------------------------------------------------------------------
|
|
127
|
+
# Inner classes
|
|
128
|
+
# ------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
class Adapter:
|
|
131
|
+
"""
|
|
132
|
+
Represents a Timberborn HTTP Adapter block.
|
|
133
|
+
|
|
134
|
+
Attributes
|
|
135
|
+
----------
|
|
136
|
+
name : str
|
|
137
|
+
Name of the adapter block in-game.
|
|
138
|
+
state : bool
|
|
139
|
+
Live-fetched state of the adapter (True = ON). Calls the API on access.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, api: "TimberbornAPI", name: str):
|
|
143
|
+
self._api = api
|
|
144
|
+
self.name = name
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def state(self) -> bool:
|
|
148
|
+
"""Live state — fetches from API on every access."""
|
|
149
|
+
return self._api.get_adapter_state(self.name)
|
|
150
|
+
|
|
151
|
+
def __repr__(self) -> str:
|
|
152
|
+
return f"Adapter(name={self.name!r})"
|
|
153
|
+
|
|
154
|
+
class Lever:
|
|
155
|
+
"""
|
|
156
|
+
Represents a Timberborn HTTP Lever block.
|
|
157
|
+
|
|
158
|
+
Attributes
|
|
159
|
+
----------
|
|
160
|
+
name : str
|
|
161
|
+
Name of the lever block in-game.
|
|
162
|
+
state : bool
|
|
163
|
+
Live-fetched state of the lever (True = ON). Calls the API on access.
|
|
164
|
+
springReturn : bool
|
|
165
|
+
Live-fetched springReturn flag. Calls the API on access.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(self, api: "TimberbornAPI", name: str):
|
|
169
|
+
self._api = api
|
|
170
|
+
self.name = name
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def state(self) -> bool:
|
|
174
|
+
"""Live state — fetches from API on every access."""
|
|
175
|
+
return self._api.get_lever_state(self.name)
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def spring_return(self) -> bool:
|
|
179
|
+
"""Live springReturn — fetches from API on every access."""
|
|
180
|
+
return self._api.get_lever_spring_return(self.name)
|
|
181
|
+
|
|
182
|
+
def switch_on(self) -> None:
|
|
183
|
+
"""Switch this lever ON."""
|
|
184
|
+
self._api.switch_on(self.name)
|
|
185
|
+
|
|
186
|
+
def switch_off(self) -> None:
|
|
187
|
+
"""Switch this lever OFF."""
|
|
188
|
+
self._api.switch_off(self.name)
|
|
189
|
+
|
|
190
|
+
def toggle(self) -> None:
|
|
191
|
+
"""Toggle this lever."""
|
|
192
|
+
self._api.toggle(self.name)
|
|
193
|
+
|
|
194
|
+
def set_color(self, hex_color: str) -> None:
|
|
195
|
+
"""Set the lever color (hex without '#', e.g. 'ff0000')."""
|
|
196
|
+
self._api.set_color(self.name, hex_color)
|
|
197
|
+
|
|
198
|
+
def __repr__(self) -> str:
|
|
199
|
+
return f"Lever(name={self.name!r})"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TimberbornWebhookServer:
|
|
203
|
+
"""
|
|
204
|
+
Webhook server that receives adapter events from Timberborn.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def __init__(self, host: str = "0.0.0.0", port: int = 8081):
|
|
208
|
+
"""
|
|
209
|
+
Create a webhook server.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
host : str
|
|
214
|
+
Host to bind to.
|
|
215
|
+
port : int
|
|
216
|
+
Port to listen on.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
self.host = host
|
|
220
|
+
self.port = port
|
|
221
|
+
|
|
222
|
+
self.app = Flask("timberborn_webhooks")
|
|
223
|
+
|
|
224
|
+
self.on_callbacks: Dict[str, Callable[[str], None]] = {}
|
|
225
|
+
self.off_callbacks: Dict[str, Callable[[str], None]] = {}
|
|
226
|
+
|
|
227
|
+
self.app.add_url_rule("/on/<name>", "on_event",
|
|
228
|
+
lambda name: self._event("ON", name),
|
|
229
|
+
methods=["GET", "POST"])
|
|
230
|
+
|
|
231
|
+
self.app.add_url_rule("/off/<name>", "off_event",
|
|
232
|
+
lambda name: self._event("OFF", name),
|
|
233
|
+
methods=["GET", "POST"])
|
|
234
|
+
|
|
235
|
+
self.start()
|
|
236
|
+
|
|
237
|
+
def _event(self, state: str, name: str):
|
|
238
|
+
name = urllib.parse.unquote(name)
|
|
239
|
+
|
|
240
|
+
logger.info("Adapter '%s' turned %s", name, state)
|
|
241
|
+
|
|
242
|
+
callbacks = self.on_callbacks if state == "ON" else self.off_callbacks
|
|
243
|
+
|
|
244
|
+
if name in callbacks:
|
|
245
|
+
callbacks[name](name)
|
|
246
|
+
|
|
247
|
+
return "OK"
|
|
248
|
+
|
|
249
|
+
@overload
|
|
250
|
+
def on_event(self, adapter_name: str) -> Callable[[
|
|
251
|
+
Callable[[str], None]], Callable[[str], None]]: ...
|
|
252
|
+
|
|
253
|
+
@overload
|
|
254
|
+
def on_event(self, adapter_name: str,
|
|
255
|
+
func: Callable[[str], None]) -> None: ...
|
|
256
|
+
|
|
257
|
+
def on_event(
|
|
258
|
+
self,
|
|
259
|
+
adapter_name: str,
|
|
260
|
+
func: Optional[Callable[[str], None]] = None
|
|
261
|
+
) -> Optional[Callable[[Callable[[str], None]], Callable[[str], None]]]:
|
|
262
|
+
"""
|
|
263
|
+
Register a callback when an adapter turns ON.
|
|
264
|
+
|
|
265
|
+
Can be used as a decorator or function call.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
if func is None:
|
|
269
|
+
def decorator(f: Callable[[str], None]) -> Callable[[str], None]:
|
|
270
|
+
self.on_callbacks[adapter_name] = f
|
|
271
|
+
return f
|
|
272
|
+
|
|
273
|
+
return decorator
|
|
274
|
+
|
|
275
|
+
self.on_callbacks[adapter_name] = func
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
@overload
|
|
279
|
+
def off_event(self, adapter_name: str) -> Callable[[
|
|
280
|
+
Callable[[str], None]], Callable[[str], None]]: ...
|
|
281
|
+
|
|
282
|
+
@overload
|
|
283
|
+
def off_event(self, adapter_name: str,
|
|
284
|
+
func: Callable[[str], None]) -> None: ...
|
|
285
|
+
|
|
286
|
+
def off_event(
|
|
287
|
+
self,
|
|
288
|
+
adapter_name: str,
|
|
289
|
+
func: Optional[Callable[[str], None]] = None
|
|
290
|
+
) -> Optional[Callable[[Callable[[str], None]], Callable[[str], None]]]:
|
|
291
|
+
"""
|
|
292
|
+
Register a callback when an adapter turns OFF.
|
|
293
|
+
|
|
294
|
+
Can be used as a decorator or function call.
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
if func is None:
|
|
298
|
+
def decorator(f: Callable[[str], None]) -> Callable[[str], None]:
|
|
299
|
+
self.off_callbacks[adapter_name] = f
|
|
300
|
+
return f
|
|
301
|
+
|
|
302
|
+
return decorator
|
|
303
|
+
|
|
304
|
+
self.off_callbacks[adapter_name] = func
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
def start(self) -> None:
|
|
308
|
+
"""Start webhook server. Can take a second or two to start."""
|
|
309
|
+
logger.info("Starting webhook server on %s:%s", self.host, self.port)
|
|
310
|
+
|
|
311
|
+
cli.show_server_banner = lambda *x: None
|
|
312
|
+
|
|
313
|
+
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
|
314
|
+
|
|
315
|
+
thread = threading.Thread(
|
|
316
|
+
target=lambda: self.app.run(
|
|
317
|
+
host=self.host,
|
|
318
|
+
port=self.port,
|
|
319
|
+
debug=False,
|
|
320
|
+
use_reloader=False
|
|
321
|
+
),
|
|
322
|
+
daemon=True,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
thread.start()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: timberborn_http
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Timberborn http api wrapper
|
|
5
5
|
Author-email: Agroqirax <124404386+Agroqirax@users.noreply.github.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -22,10 +22,9 @@ Supports both direct API control and webhook-based event handling.
|
|
|
22
22
|
|
|
23
23
|
## Features
|
|
24
24
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
- **Multiple interaction styles**: OOP, decorators, and direct function calls
|
|
25
|
+
- Get the state of levers & adapters from timberborn
|
|
26
|
+
- Update the state of levers
|
|
27
|
+
- Receive webhooks and trigger actions
|
|
29
28
|
|
|
30
29
|
## Installation
|
|
31
30
|
|
|
@@ -48,8 +47,8 @@ for lever in levers:
|
|
|
48
47
|
print(lever.name, lever.state)
|
|
49
48
|
|
|
50
49
|
# Control a lever
|
|
51
|
-
api.switch_on("
|
|
52
|
-
api.set_color("
|
|
50
|
+
api.switch_on("HTTP Lever 1")
|
|
51
|
+
api.set_color("HTTP Lever 1", "ff0000")
|
|
53
52
|
```
|
|
54
53
|
|
|
55
54
|
### 2. Webhook Events
|
|
@@ -57,30 +56,19 @@ api.set_color("Main Water Pump", "ff0000")
|
|
|
57
56
|
```python
|
|
58
57
|
from timberborn_http import TimberbornWebhookServer
|
|
59
58
|
|
|
60
|
-
server = TimberbornWebhookServer()
|
|
59
|
+
server = TimberbornWebhookServer(port=8081)
|
|
61
60
|
|
|
62
|
-
@server.
|
|
61
|
+
@server.on_event("HTTP Lever 1")
|
|
63
62
|
def handle_on(name):
|
|
64
63
|
print(f"{name} turned ON!")
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
while True:
|
|
66
|
+
pass
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
- [API Usage](/examples/api): Direct control and polling
|
|
72
|
-
- [Webhook Usage](/examples/webhooks): Real-time event handling
|
|
69
|
+
More examples & details in the examples folder
|
|
73
70
|
|
|
74
71
|
## Getting Help
|
|
75
72
|
|
|
76
73
|
- [GitHub Issues](https://github.com/agroqirax/timberborn-http/issues)
|
|
77
|
-
- [Discord](https://discord.gg/timberborn)
|
|
78
|
-
|
|
79
|
-
## Commands
|
|
80
|
-
|
|
81
|
-
- `python -m venv .venv`
|
|
82
|
-
- `pip install -r requirements.txt`
|
|
83
|
-
- `pip install -e .`
|
|
84
|
-
- `python -m build`
|
|
85
|
-
- `python3 -m twine upload --repository testpypi dist/*`
|
|
86
|
-
- `python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps timberborn_http`
|
|
74
|
+
- [Discord](https://discord.gg/timberborn) in `#⏱️automation` or `#🤖mod-creators`
|
timberborn_http-0.0.2/README.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# Timberborn HTTP API Wrapper
|
|
2
|
-
|
|
3
|
-
A Python library for interacting with the Timberborn HTTP automation API.
|
|
4
|
-
Supports both direct API control and webhook-based event handling.
|
|
5
|
-
|
|
6
|
-
## Features
|
|
7
|
-
|
|
8
|
-
- **Control levers and adapters** in your Timberborn game
|
|
9
|
-
- **Poll or watch** for state changes
|
|
10
|
-
- **Webhook support** for real-time events
|
|
11
|
-
- **Multiple interaction styles**: OOP, decorators, and direct function calls
|
|
12
|
-
|
|
13
|
-
## Installation
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
pip install timberborn-http
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Quickstart
|
|
20
|
-
|
|
21
|
-
### 1. Direct API Control
|
|
22
|
-
|
|
23
|
-
```python
|
|
24
|
-
from timberborn_http import TimberbornAPI
|
|
25
|
-
|
|
26
|
-
api = TimberbornAPI("http://localhost:8080")
|
|
27
|
-
|
|
28
|
-
# Get all levers
|
|
29
|
-
levers = api.get_levers()
|
|
30
|
-
for lever in levers:
|
|
31
|
-
print(lever.name, lever.state)
|
|
32
|
-
|
|
33
|
-
# Control a lever
|
|
34
|
-
api.switch_on("Main Water Pump")
|
|
35
|
-
api.set_color("Main Water Pump", "ff0000")
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### 2. Webhook Events
|
|
39
|
-
|
|
40
|
-
```python
|
|
41
|
-
from timberborn_http import TimberbornWebhookServer
|
|
42
|
-
|
|
43
|
-
server = TimberbornWebhookServer()
|
|
44
|
-
|
|
45
|
-
@server.on("Main Water Pump")
|
|
46
|
-
def handle_on(name):
|
|
47
|
-
print(f"{name} turned ON!")
|
|
48
|
-
|
|
49
|
-
server.start()
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Documentation & Examples
|
|
53
|
-
|
|
54
|
-
- [API Usage](/examples/api): Direct control and polling
|
|
55
|
-
- [Webhook Usage](/examples/webhooks): Real-time event handling
|
|
56
|
-
|
|
57
|
-
## Getting Help
|
|
58
|
-
|
|
59
|
-
- [GitHub Issues](https://github.com/agroqirax/timberborn-http/issues)
|
|
60
|
-
- [Discord](https://discord.gg/timberborn)
|
|
61
|
-
|
|
62
|
-
## Commands
|
|
63
|
-
|
|
64
|
-
- `python -m venv .venv`
|
|
65
|
-
- `pip install -r requirements.txt`
|
|
66
|
-
- `pip install -e .`
|
|
67
|
-
- `python -m build`
|
|
68
|
-
- `python3 -m twine upload --repository testpypi dist/*`
|
|
69
|
-
- `python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps timberborn_http`
|
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass, field
|
|
2
|
-
from typing import Callable, Dict, List, Optional
|
|
3
|
-
import logging
|
|
4
|
-
import threading
|
|
5
|
-
import time
|
|
6
|
-
import urllib.parse
|
|
7
|
-
|
|
8
|
-
import requests
|
|
9
|
-
from flask import Flask
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger("timberborn_http")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# ============================================================
|
|
15
|
-
# Data Classes
|
|
16
|
-
# ============================================================
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@dataclass
|
|
20
|
-
class Adapter:
|
|
21
|
-
"""
|
|
22
|
-
Represents a Timberborn HTTP Adapter block.
|
|
23
|
-
|
|
24
|
-
Attributes
|
|
25
|
-
----------
|
|
26
|
-
name : str
|
|
27
|
-
Name of the adapter block in-game.
|
|
28
|
-
state : bool
|
|
29
|
-
Current state of the adapter (True = ON).
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
name: str
|
|
33
|
-
state: bool
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@dataclass
|
|
37
|
-
class Lever:
|
|
38
|
-
"""
|
|
39
|
-
Represents a Timberborn HTTP Lever block.
|
|
40
|
-
|
|
41
|
-
Attributes
|
|
42
|
-
----------
|
|
43
|
-
name : str
|
|
44
|
-
Name of the lever block in-game.
|
|
45
|
-
state : bool
|
|
46
|
-
Current state of the lever (True = ON).
|
|
47
|
-
springReturn : bool
|
|
48
|
-
Whether the lever automatically resets.
|
|
49
|
-
api : TimberbornAPI
|
|
50
|
-
Reference to the API client used for control.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
name: str
|
|
54
|
-
state: bool
|
|
55
|
-
springReturn: bool
|
|
56
|
-
api: "TimberbornAPI" = field(repr=False)
|
|
57
|
-
|
|
58
|
-
# ------------------------------------------
|
|
59
|
-
|
|
60
|
-
def on(self) -> None:
|
|
61
|
-
"""Switch this lever ON."""
|
|
62
|
-
self.api.switch_on(self.name)
|
|
63
|
-
|
|
64
|
-
def off(self) -> None:
|
|
65
|
-
"""Switch this lever OFF."""
|
|
66
|
-
self.api.switch_off(self.name)
|
|
67
|
-
|
|
68
|
-
def toggle(self) -> None:
|
|
69
|
-
"""Toggle this lever."""
|
|
70
|
-
self.api.toggle(self.name)
|
|
71
|
-
|
|
72
|
-
def set_color(self, hex_color: str) -> None:
|
|
73
|
-
"""
|
|
74
|
-
Set the lever color.
|
|
75
|
-
|
|
76
|
-
Parameters
|
|
77
|
-
----------
|
|
78
|
-
hex_color : str
|
|
79
|
-
Hex color code (e.g. 'ff0000').
|
|
80
|
-
"""
|
|
81
|
-
self.api.set_color(self.name, hex_color)
|
|
82
|
-
|
|
83
|
-
def refresh(self) -> None:
|
|
84
|
-
"""Refresh the lever state from the API."""
|
|
85
|
-
self.state = self.api.get_lever_state(self.name)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# ============================================================
|
|
89
|
-
# API Client
|
|
90
|
-
# ============================================================
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class TimberbornAPI:
|
|
94
|
-
"""
|
|
95
|
-
Client for interacting with the Timberborn HTTP automation API.
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
def __init__(self, host: str = "http://localhost:8080"):
|
|
99
|
-
"""
|
|
100
|
-
Initialize the API client.
|
|
101
|
-
|
|
102
|
-
Parameters
|
|
103
|
-
----------
|
|
104
|
-
host : str
|
|
105
|
-
Base URL of the Timberborn API.
|
|
106
|
-
"""
|
|
107
|
-
self.host = host.rstrip("/")
|
|
108
|
-
|
|
109
|
-
# ------------------------------------------
|
|
110
|
-
|
|
111
|
-
def _encode(self, name: str) -> str:
|
|
112
|
-
"""URL-encode a block name."""
|
|
113
|
-
return urllib.parse.quote(name)
|
|
114
|
-
|
|
115
|
-
# ------------------------------------------
|
|
116
|
-
# Query
|
|
117
|
-
# ------------------------------------------
|
|
118
|
-
|
|
119
|
-
def get_adapters(self) -> List[Adapter]:
|
|
120
|
-
"""
|
|
121
|
-
Retrieve all adapters.
|
|
122
|
-
|
|
123
|
-
Returns
|
|
124
|
-
-------
|
|
125
|
-
List[Adapter]
|
|
126
|
-
List of adapters.
|
|
127
|
-
"""
|
|
128
|
-
logger.debug("Fetching adapters")
|
|
129
|
-
|
|
130
|
-
r = requests.get(f"{self.host}/api/adapters")
|
|
131
|
-
r.raise_for_status()
|
|
132
|
-
|
|
133
|
-
data = r.json()
|
|
134
|
-
|
|
135
|
-
return [Adapter(**adapter) for adapter in data]
|
|
136
|
-
|
|
137
|
-
def get_levers(self) -> List[Lever]:
|
|
138
|
-
"""
|
|
139
|
-
Retrieve all levers.
|
|
140
|
-
|
|
141
|
-
Returns
|
|
142
|
-
-------
|
|
143
|
-
List[Lever]
|
|
144
|
-
List of lever objects.
|
|
145
|
-
"""
|
|
146
|
-
logger.debug("Fetching levers")
|
|
147
|
-
|
|
148
|
-
r = requests.get(f"{self.host}/api/levers")
|
|
149
|
-
r.raise_for_status()
|
|
150
|
-
|
|
151
|
-
data = r.json()
|
|
152
|
-
|
|
153
|
-
return [Lever(**lever, api=self) for lever in data]
|
|
154
|
-
|
|
155
|
-
# ------------------------------------------
|
|
156
|
-
# State helpers
|
|
157
|
-
# ------------------------------------------
|
|
158
|
-
|
|
159
|
-
def get_adapter_state(self, name: str) -> bool:
|
|
160
|
-
"""
|
|
161
|
-
Get adapter state.
|
|
162
|
-
|
|
163
|
-
Parameters
|
|
164
|
-
----------
|
|
165
|
-
name : str
|
|
166
|
-
Adapter name.
|
|
167
|
-
|
|
168
|
-
Returns
|
|
169
|
-
-------
|
|
170
|
-
bool
|
|
171
|
-
Adapter state.
|
|
172
|
-
"""
|
|
173
|
-
for adapter in self.get_adapters():
|
|
174
|
-
if adapter.name == name:
|
|
175
|
-
return adapter.state
|
|
176
|
-
|
|
177
|
-
raise ValueError(f"Adapter '{name}' not found")
|
|
178
|
-
|
|
179
|
-
def get_lever_state(self, name: str) -> bool:
|
|
180
|
-
"""
|
|
181
|
-
Get lever state.
|
|
182
|
-
|
|
183
|
-
Parameters
|
|
184
|
-
----------
|
|
185
|
-
name : str
|
|
186
|
-
Lever name.
|
|
187
|
-
|
|
188
|
-
Returns
|
|
189
|
-
-------
|
|
190
|
-
bool
|
|
191
|
-
Lever state.
|
|
192
|
-
"""
|
|
193
|
-
for lever in self.get_levers():
|
|
194
|
-
if lever.name == name:
|
|
195
|
-
return lever.state
|
|
196
|
-
|
|
197
|
-
raise ValueError(f"Lever '{name}' not found")
|
|
198
|
-
|
|
199
|
-
# ------------------------------------------
|
|
200
|
-
# Control
|
|
201
|
-
# ------------------------------------------
|
|
202
|
-
|
|
203
|
-
def switch_on(self, name: str) -> None:
|
|
204
|
-
"""
|
|
205
|
-
Switch a lever ON.
|
|
206
|
-
|
|
207
|
-
Parameters
|
|
208
|
-
----------
|
|
209
|
-
name : str
|
|
210
|
-
Lever name.
|
|
211
|
-
"""
|
|
212
|
-
logger.info("Switching ON lever '%s'", name)
|
|
213
|
-
|
|
214
|
-
name = self._encode(name)
|
|
215
|
-
requests.get(f"{self.host}/api/switch-on/{name}").raise_for_status()
|
|
216
|
-
|
|
217
|
-
def switch_off(self, name: str) -> None:
|
|
218
|
-
"""
|
|
219
|
-
Switch a lever OFF.
|
|
220
|
-
|
|
221
|
-
Parameters
|
|
222
|
-
----------
|
|
223
|
-
name : str
|
|
224
|
-
Lever name.
|
|
225
|
-
"""
|
|
226
|
-
logger.info("Switching OFF lever '%s'", name)
|
|
227
|
-
|
|
228
|
-
name = self._encode(name)
|
|
229
|
-
requests.get(f"{self.host}/api/switch-off/{name}").raise_for_status()
|
|
230
|
-
|
|
231
|
-
def toggle(self, name: str) -> None:
|
|
232
|
-
"""
|
|
233
|
-
Toggle a lever state.
|
|
234
|
-
|
|
235
|
-
Parameters
|
|
236
|
-
----------
|
|
237
|
-
name : str
|
|
238
|
-
Lever name.
|
|
239
|
-
"""
|
|
240
|
-
logger.info("Toggling lever '%s'", name)
|
|
241
|
-
|
|
242
|
-
if self.get_lever_state(name):
|
|
243
|
-
self.switch_off(name)
|
|
244
|
-
else:
|
|
245
|
-
self.switch_on(name)
|
|
246
|
-
|
|
247
|
-
def set_color(self, name: str, hex_color: str) -> None:
|
|
248
|
-
"""
|
|
249
|
-
Set lever color.
|
|
250
|
-
|
|
251
|
-
Parameters
|
|
252
|
-
----------
|
|
253
|
-
name : str
|
|
254
|
-
Lever name.
|
|
255
|
-
hex_color : str
|
|
256
|
-
Hex color (e.g. '00ff00').
|
|
257
|
-
"""
|
|
258
|
-
logger.info("Setting color of '%s' to %s", name, hex_color)
|
|
259
|
-
|
|
260
|
-
name = self._encode(name)
|
|
261
|
-
requests.get(
|
|
262
|
-
f"{self.host}/api/color/{name}/{hex_color}"
|
|
263
|
-
).raise_for_status()
|
|
264
|
-
|
|
265
|
-
# ------------------------------------------
|
|
266
|
-
# Polling Watcher
|
|
267
|
-
# ------------------------------------------
|
|
268
|
-
|
|
269
|
-
def watch_adapter(
|
|
270
|
-
self,
|
|
271
|
-
name: str,
|
|
272
|
-
callback: Callable[[str, bool], None],
|
|
273
|
-
poll_interval: float = 1.0,
|
|
274
|
-
) -> None:
|
|
275
|
-
"""
|
|
276
|
-
Watch an adapter and trigger a callback when its state changes.
|
|
277
|
-
|
|
278
|
-
Parameters
|
|
279
|
-
----------
|
|
280
|
-
name : str
|
|
281
|
-
Adapter name.
|
|
282
|
-
callback : Callable[[str, bool], None]
|
|
283
|
-
Function called with (name, state).
|
|
284
|
-
poll_interval : float
|
|
285
|
-
Seconds between polls.
|
|
286
|
-
"""
|
|
287
|
-
|
|
288
|
-
def watcher():
|
|
289
|
-
logger.info("Watching adapter '%s'", name)
|
|
290
|
-
|
|
291
|
-
last_state: Optional[bool] = None
|
|
292
|
-
|
|
293
|
-
while True:
|
|
294
|
-
state = self.get_adapter_state(name)
|
|
295
|
-
|
|
296
|
-
if last_state is None:
|
|
297
|
-
last_state = state
|
|
298
|
-
|
|
299
|
-
if state != last_state:
|
|
300
|
-
callback(name, state)
|
|
301
|
-
last_state = state
|
|
302
|
-
|
|
303
|
-
time.sleep(poll_interval)
|
|
304
|
-
|
|
305
|
-
thread = threading.Thread(target=watcher, daemon=True)
|
|
306
|
-
thread.start()
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
# ============================================================
|
|
310
|
-
# Webhook Server
|
|
311
|
-
# ============================================================
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
class TimberbornWebhookServer:
|
|
315
|
-
"""
|
|
316
|
-
Webhook server that receives adapter events from Timberborn.
|
|
317
|
-
"""
|
|
318
|
-
|
|
319
|
-
def __init__(self, host: str = "0.0.0.0", port: int = 8081):
|
|
320
|
-
"""
|
|
321
|
-
Create a webhook server.
|
|
322
|
-
|
|
323
|
-
Parameters
|
|
324
|
-
----------
|
|
325
|
-
host : str
|
|
326
|
-
Host to bind to.
|
|
327
|
-
port : int
|
|
328
|
-
Port to listen on.
|
|
329
|
-
"""
|
|
330
|
-
|
|
331
|
-
self.host = host
|
|
332
|
-
self.port = port
|
|
333
|
-
|
|
334
|
-
self.app = Flask("timberborn_webhooks")
|
|
335
|
-
|
|
336
|
-
self.on_callbacks: Dict[str, Callable[[str], None]] = {}
|
|
337
|
-
self.off_callbacks: Dict[str, Callable[[str], None]] = {}
|
|
338
|
-
|
|
339
|
-
self.app.add_url_rule("/on/<name>", "on_event",
|
|
340
|
-
self._on_event, methods=["GET", "POST"])
|
|
341
|
-
self.app.add_url_rule("/off/<name>", "off_event",
|
|
342
|
-
self._off_event, methods=["GET", "POST"])
|
|
343
|
-
|
|
344
|
-
# ------------------------------------------
|
|
345
|
-
|
|
346
|
-
def _on_event(self, name: str) -> str:
|
|
347
|
-
name = urllib.parse.unquote(name)
|
|
348
|
-
|
|
349
|
-
logger.info("Adapter '%s' turned ON", name)
|
|
350
|
-
|
|
351
|
-
if name in self.on_callbacks:
|
|
352
|
-
self.on_callbacks[name](name)
|
|
353
|
-
|
|
354
|
-
return "OK"
|
|
355
|
-
|
|
356
|
-
def _off_event(self, name: str) -> str:
|
|
357
|
-
name = urllib.parse.unquote(name)
|
|
358
|
-
|
|
359
|
-
logger.info("Adapter '%s' turned OFF", name)
|
|
360
|
-
|
|
361
|
-
if name in self.off_callbacks:
|
|
362
|
-
self.off_callbacks[name](name)
|
|
363
|
-
|
|
364
|
-
return "OK"
|
|
365
|
-
|
|
366
|
-
# ------------------------------------------
|
|
367
|
-
# Registration
|
|
368
|
-
# ------------------------------------------
|
|
369
|
-
|
|
370
|
-
def on(self, adapter_name: str, func: Optional[Callable[[str], None]] = None):
|
|
371
|
-
"""
|
|
372
|
-
Register a callback when an adapter turns ON.
|
|
373
|
-
|
|
374
|
-
Can be used as a decorator or function call.
|
|
375
|
-
"""
|
|
376
|
-
|
|
377
|
-
if func is None:
|
|
378
|
-
|
|
379
|
-
def decorator(f):
|
|
380
|
-
self.on_callbacks[adapter_name] = f
|
|
381
|
-
return f
|
|
382
|
-
|
|
383
|
-
return decorator
|
|
384
|
-
|
|
385
|
-
self.on_callbacks[adapter_name] = func
|
|
386
|
-
|
|
387
|
-
def off(self, adapter_name: str, func: Optional[Callable[[str], None]] = None):
|
|
388
|
-
"""
|
|
389
|
-
Register a callback when an adapter turns OFF.
|
|
390
|
-
|
|
391
|
-
Can be used as a decorator or function call.
|
|
392
|
-
"""
|
|
393
|
-
|
|
394
|
-
if func is None:
|
|
395
|
-
|
|
396
|
-
def decorator(f):
|
|
397
|
-
self.off_callbacks[adapter_name] = f
|
|
398
|
-
return f
|
|
399
|
-
|
|
400
|
-
return decorator
|
|
401
|
-
|
|
402
|
-
self.off_callbacks[adapter_name] = func
|
|
403
|
-
|
|
404
|
-
# ------------------------------------------
|
|
405
|
-
|
|
406
|
-
def start(self) -> None:
|
|
407
|
-
"""Start the webhook server in a background thread."""
|
|
408
|
-
|
|
409
|
-
logger.info("Starting webhook server on %s:%s", self.host, self.port)
|
|
410
|
-
|
|
411
|
-
thread = threading.Thread(
|
|
412
|
-
target=self.app.run,
|
|
413
|
-
kwargs={"host": self.host, "port": self.port},
|
|
414
|
-
daemon=True,
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
thread.start()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{timberborn_http-0.0.2 → timberborn_http-0.0.4}/src/timberborn_http.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|