tinytoolslib 0.2.5__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.
- tinytoolslib/__init__.py +1 -0
- tinytoolslib/__version__.py +1 -0
- tinytoolslib/constants.py +14 -0
- tinytoolslib/discovery.py +167 -0
- tinytoolslib/exceptions.py +49 -0
- tinytoolslib/flash.py +316 -0
- tinytoolslib/models.py +1674 -0
- tinytoolslib/parsers.py +86 -0
- tinytoolslib/requests.py +268 -0
- tinytoolslib-0.2.5.dist-info/METADATA +76 -0
- tinytoolslib-0.2.5.dist-info/RECORD +13 -0
- tinytoolslib-0.2.5.dist-info/WHEEL +5 -0
- tinytoolslib-0.2.5.dist-info/top_level.txt +1 -0
tinytoolslib/parsers.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def parse_version(data):
|
|
6
|
+
"""Parse version info from data."""
|
|
7
|
+
hw = None
|
|
8
|
+
sw = None
|
|
9
|
+
if "ver" in data:
|
|
10
|
+
# LK2.X
|
|
11
|
+
hw = "2." + data.get("hw")
|
|
12
|
+
sw = data.get("ver")
|
|
13
|
+
elif "sw" in data:
|
|
14
|
+
# LK3.X
|
|
15
|
+
hw = data.get("hw")
|
|
16
|
+
sw = data.get("sw")
|
|
17
|
+
elif "hardwareVersion" in data:
|
|
18
|
+
hw = data.get("hardwareVersion")
|
|
19
|
+
sw = data.get("softwareVersion")
|
|
20
|
+
return {
|
|
21
|
+
"hardware_version": hw,
|
|
22
|
+
"software_version": sw,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def int_inverted(value):
|
|
27
|
+
"""Invert 0/1 value."""
|
|
28
|
+
return int(not int(value))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def up_to_int(value):
|
|
32
|
+
"""Convert text 'up' to 1 for LK2.5"""
|
|
33
|
+
if value == "up":
|
|
34
|
+
return 1
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def float_div10(value):
|
|
39
|
+
"""Convert string number to float divided by 10"""
|
|
40
|
+
return float(value) / 10
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def float_div100(value):
|
|
44
|
+
"""Convert string number to float divided by 100"""
|
|
45
|
+
return float(value) / 100
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def float_div1000(value):
|
|
49
|
+
"""Convert string number to float divided by 1000"""
|
|
50
|
+
return float(value) / 1000
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def str_datetime(value):
|
|
54
|
+
"""Convert string timestamp to datetime"""
|
|
55
|
+
return datetime.fromtimestamp(int(value))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def strint_to_int_list(length):
|
|
59
|
+
"""Convert string with number to list of ints (one for bit)"""
|
|
60
|
+
return lambda x: list(
|
|
61
|
+
reversed([int(item) for item in bin(int(x)).lstrip("-0b").zfill(length)])
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def strdot_to_int_list(value):
|
|
66
|
+
"""Convert string with numbers divided with '*' to list of ints"""
|
|
67
|
+
return [int(item) for item in value.split("*")]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def int_mul(value, multiplier):
|
|
71
|
+
"""Multiply value and return int."""
|
|
72
|
+
return int(value * multiplier)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def name_list(name, count, start=1):
|
|
76
|
+
"""Return list of names with included index."""
|
|
77
|
+
tag = "{}"
|
|
78
|
+
if tag not in name:
|
|
79
|
+
name = name + tag
|
|
80
|
+
return [name.format(i) for i in range(start, start + count)]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@lru_cache(None)
|
|
84
|
+
def list_map(type_):
|
|
85
|
+
"""Return function for mapping list items to given type."""
|
|
86
|
+
return lambda x: list(map(type_, x))
|
tinytoolslib/requests.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Union
|
|
3
|
+
import socket
|
|
4
|
+
from xml.etree import ElementTree
|
|
5
|
+
import requests
|
|
6
|
+
from functools import wraps
|
|
7
|
+
import asyncio
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
TinyToolsRequestConnectionError,
|
|
10
|
+
TinyToolsRequestError,
|
|
11
|
+
TinyToolsRequestHTTPError,
|
|
12
|
+
TinyToolsRequestInternalServerError,
|
|
13
|
+
TinyToolsRequestNotFound,
|
|
14
|
+
TinyToolsRequestSSLError,
|
|
15
|
+
TinyToolsRequestTimeout,
|
|
16
|
+
TinyToolsRequestUnauthenticated,
|
|
17
|
+
)
|
|
18
|
+
from aiohttp import (
|
|
19
|
+
ClientSession,
|
|
20
|
+
BasicAuth,
|
|
21
|
+
ClientTimeout,
|
|
22
|
+
ClientConnectionError,
|
|
23
|
+
ClientConnectorCertificateError,
|
|
24
|
+
ClientOSError,
|
|
25
|
+
ClientResponseError,
|
|
26
|
+
InvalidURL,
|
|
27
|
+
ServerDisconnectedError,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
DEFAULT_TIMEOUT = 3
|
|
32
|
+
DEFAULT_RETRIES = 1
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_parameters(
|
|
36
|
+
host, path, schema, port, username, password, timeout, retries, verify
|
|
37
|
+
):
|
|
38
|
+
"""Parse parameters for request - url, auth, timeout."""
|
|
39
|
+
# Small modification to also handle general purpose requests
|
|
40
|
+
if (host.startswith("http://") or host.startswith("https://")) and path is None:
|
|
41
|
+
url = host
|
|
42
|
+
# Always verify when given complete URL
|
|
43
|
+
verify = True
|
|
44
|
+
else:
|
|
45
|
+
url = f"{schema}://{host}:{port}{path}"
|
|
46
|
+
if verify is None:
|
|
47
|
+
# Disable ssl verification for tinycontrol devices, which by default use insecure
|
|
48
|
+
# certificates, so they can be easily handled with library.
|
|
49
|
+
verify = schema != "https"
|
|
50
|
+
if username:
|
|
51
|
+
auth = (username, password)
|
|
52
|
+
else:
|
|
53
|
+
auth = None
|
|
54
|
+
if timeout is None:
|
|
55
|
+
timeout = DEFAULT_TIMEOUT
|
|
56
|
+
if retries is None:
|
|
57
|
+
retries = DEFAULT_RETRIES
|
|
58
|
+
return url, auth, timeout, retries, verify
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _handle_errors(exc, response=None):
|
|
62
|
+
"""Handle errors for request.
|
|
63
|
+
|
|
64
|
+
Unifies errors for asyncio and basic version.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
raise exc
|
|
68
|
+
except (requests.exceptions.ConnectTimeout, asyncio.TimeoutError) as exc:
|
|
69
|
+
raise TinyToolsRequestTimeout("Timed out") from exc
|
|
70
|
+
except (requests.exceptions.SSLError, ClientConnectorCertificateError) as exc:
|
|
71
|
+
raise TinyToolsRequestSSLError("SSL error") from exc
|
|
72
|
+
except (
|
|
73
|
+
requests.exceptions.ConnectionError,
|
|
74
|
+
ClientConnectionError,
|
|
75
|
+
socket.gaierror,
|
|
76
|
+
) as exc:
|
|
77
|
+
raise TinyToolsRequestConnectionError("Connection error") from exc
|
|
78
|
+
except (requests.exceptions.InvalidURL, InvalidURL) as exc:
|
|
79
|
+
raise TinyToolsRequestError("Invalid URL") from exc
|
|
80
|
+
except (requests.exceptions.HTTPError, ClientResponseError) as exc:
|
|
81
|
+
if hasattr(response, "status_code"):
|
|
82
|
+
status_code = response.status_code
|
|
83
|
+
else: # For aiohttp it will be status
|
|
84
|
+
status_code = response.status
|
|
85
|
+
if status_code == 401:
|
|
86
|
+
raise TinyToolsRequestUnauthenticated("Authentication error") from exc
|
|
87
|
+
elif status_code == 404:
|
|
88
|
+
raise TinyToolsRequestNotFound("Not found") from exc
|
|
89
|
+
elif status_code == 500:
|
|
90
|
+
raise TinyToolsRequestInternalServerError("Server error") from exc
|
|
91
|
+
else:
|
|
92
|
+
raise TinyToolsRequestHTTPError(f"HTTP {status_code} error") from exc
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
raise TinyToolsRequestError("Unexpected request error") from exc
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _handle_content(content_type, content, response):
|
|
98
|
+
"""Handle response content and pack it into TinyToolsResponse."""
|
|
99
|
+
result = {
|
|
100
|
+
"_response": response,
|
|
101
|
+
"parsed": None,
|
|
102
|
+
}
|
|
103
|
+
# TODO: Add ping for requests method (ATM missing for aiohttp).
|
|
104
|
+
if hasattr(response, "elapsed"):
|
|
105
|
+
result["ping"] = round(response.elapsed.total_seconds() * 1000, 3)
|
|
106
|
+
try:
|
|
107
|
+
if content_type == "application/json":
|
|
108
|
+
result["parsed"] = json.loads(content)
|
|
109
|
+
elif content_type == "text/html":
|
|
110
|
+
result["parsed"] = {}
|
|
111
|
+
elif content_type == "text/xml":
|
|
112
|
+
result["parsed"] = {
|
|
113
|
+
item.tag: item.text for item in ElementTree.fromstring(content)
|
|
114
|
+
}
|
|
115
|
+
except (ElementTree.ParseError, ValueError) as exc:
|
|
116
|
+
raise TinyToolsRequestError("Cannot parse response") from exc
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def request(
|
|
121
|
+
method,
|
|
122
|
+
host,
|
|
123
|
+
path,
|
|
124
|
+
schema="http",
|
|
125
|
+
port=80,
|
|
126
|
+
username="",
|
|
127
|
+
password="",
|
|
128
|
+
timeout=None,
|
|
129
|
+
retries=None,
|
|
130
|
+
verify=None,
|
|
131
|
+
data=None,
|
|
132
|
+
headers=None,
|
|
133
|
+
):
|
|
134
|
+
"""Send request to device and return dict with response or errors."""
|
|
135
|
+
url, auth, timeout, retries, verify = _parse_parameters(
|
|
136
|
+
host, path, schema, port, username, password, timeout, retries, verify
|
|
137
|
+
)
|
|
138
|
+
response = None
|
|
139
|
+
try:
|
|
140
|
+
response = requests.request(
|
|
141
|
+
method,
|
|
142
|
+
url,
|
|
143
|
+
auth=auth,
|
|
144
|
+
data=data,
|
|
145
|
+
headers=headers,
|
|
146
|
+
timeout=timeout,
|
|
147
|
+
verify=verify,
|
|
148
|
+
)
|
|
149
|
+
response.raise_for_status()
|
|
150
|
+
except requests.exceptions.ConnectTimeout as exc:
|
|
151
|
+
if retries:
|
|
152
|
+
return request(
|
|
153
|
+
method,
|
|
154
|
+
host,
|
|
155
|
+
path,
|
|
156
|
+
schema,
|
|
157
|
+
port,
|
|
158
|
+
username,
|
|
159
|
+
password,
|
|
160
|
+
timeout,
|
|
161
|
+
retries - 1,
|
|
162
|
+
verify,
|
|
163
|
+
data,
|
|
164
|
+
headers,
|
|
165
|
+
)
|
|
166
|
+
_handle_errors(exc, response)
|
|
167
|
+
except Exception as exc:
|
|
168
|
+
_handle_errors(exc, response)
|
|
169
|
+
else:
|
|
170
|
+
return _handle_content(
|
|
171
|
+
response.headers.get("content-type"), response.content, response
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@wraps(request)
|
|
176
|
+
def get(*args, silent=False, **kwargs):
|
|
177
|
+
"""Send GET request."""
|
|
178
|
+
try:
|
|
179
|
+
return request("GET", *args, **kwargs)
|
|
180
|
+
except TinyToolsRequestError:
|
|
181
|
+
if silent:
|
|
182
|
+
return None
|
|
183
|
+
raise
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@wraps(request)
|
|
187
|
+
def post(*args, **kwargs):
|
|
188
|
+
"""Send POST request."""
|
|
189
|
+
return request(
|
|
190
|
+
"POST",
|
|
191
|
+
*args,
|
|
192
|
+
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
|
|
193
|
+
**kwargs,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
async def async_request(
|
|
198
|
+
method,
|
|
199
|
+
host,
|
|
200
|
+
path,
|
|
201
|
+
schema="http",
|
|
202
|
+
port=80,
|
|
203
|
+
username=None,
|
|
204
|
+
password=None,
|
|
205
|
+
timeout=None,
|
|
206
|
+
retries=None,
|
|
207
|
+
verify=None,
|
|
208
|
+
params=None,
|
|
209
|
+
data=None,
|
|
210
|
+
headers=None,
|
|
211
|
+
session: Union[ClientSession, None] = None,
|
|
212
|
+
):
|
|
213
|
+
"""Async request with aiohttp for given resource.
|
|
214
|
+
|
|
215
|
+
It can return text or json-parsed response, else raise exception.
|
|
216
|
+
"""
|
|
217
|
+
url, auth, timeout, retries, verify = _parse_parameters(
|
|
218
|
+
host, path, schema, port, username, password, timeout, retries, verify
|
|
219
|
+
)
|
|
220
|
+
if auth:
|
|
221
|
+
auth = BasicAuth(*auth)
|
|
222
|
+
response = None
|
|
223
|
+
try:
|
|
224
|
+
async with session.request(
|
|
225
|
+
method,
|
|
226
|
+
url,
|
|
227
|
+
auth=auth,
|
|
228
|
+
params=params,
|
|
229
|
+
timeout=ClientTimeout(total=timeout),
|
|
230
|
+
verify_ssl=verify,
|
|
231
|
+
) as response:
|
|
232
|
+
response.raise_for_status()
|
|
233
|
+
content = await response.text()
|
|
234
|
+
except (asyncio.TimeoutError, ClientOSError, ServerDisconnectedError) as exc:
|
|
235
|
+
# Retry only on timeout and few specific client connection errors
|
|
236
|
+
# (they happen randomly at least on Windows).
|
|
237
|
+
if retries:
|
|
238
|
+
return await async_request(
|
|
239
|
+
method,
|
|
240
|
+
host,
|
|
241
|
+
path,
|
|
242
|
+
schema,
|
|
243
|
+
port,
|
|
244
|
+
username,
|
|
245
|
+
password,
|
|
246
|
+
timeout,
|
|
247
|
+
retries - 1,
|
|
248
|
+
verify,
|
|
249
|
+
params,
|
|
250
|
+
data,
|
|
251
|
+
headers,
|
|
252
|
+
session,
|
|
253
|
+
)
|
|
254
|
+
_handle_errors(exc, response)
|
|
255
|
+
except Exception as exc:
|
|
256
|
+
_handle_errors(exc, response)
|
|
257
|
+
return _handle_content(response.content_type, content, response)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@wraps(async_request)
|
|
261
|
+
async def async_get(*args, silent=False, **kwargs):
|
|
262
|
+
"""Send async GET request."""
|
|
263
|
+
try:
|
|
264
|
+
return await async_request("GET", *args, **kwargs)
|
|
265
|
+
except TinyToolsRequestError:
|
|
266
|
+
if silent:
|
|
267
|
+
return None
|
|
268
|
+
raise
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tinytoolslib
|
|
3
|
+
Version: 0.2.5
|
|
4
|
+
Summary: Set of tools for use with Tinycontrol devices like LK2.X, LK3.X, LK4.X or tcPDU.
|
|
5
|
+
Author-email: Bartek Barszczewski <tinycontrol.software@gmail.com>
|
|
6
|
+
Keywords: tinycontrol,lk,tcpdu
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Requires-Python: >=3.7
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: aiohttp <4,>=3.9.3
|
|
13
|
+
Requires-Dist: netifaces <1,>=0.11.0
|
|
14
|
+
Requires-Dist: requests <3,>=2.31.0
|
|
15
|
+
Requires-Dist: tftpy <1,>=0.8.2
|
|
16
|
+
|
|
17
|
+
# tinyToolsLib
|
|
18
|
+
|
|
19
|
+
Set of tools for use with tinycontrol devices like LK2.X, LK3.X, LK4.X or tcPDU.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
Easy to use functions for common actions with tinycontrol devices:
|
|
24
|
+
|
|
25
|
+
- Flashing firmware via TFTP (LK2.X, LK3.X).
|
|
26
|
+
- Flashing firmware via HTTP (LK4, tcPDU).
|
|
27
|
+
- Getting data from devices.
|
|
28
|
+
- Sending commands to devices (for common tasks like controlling OUTs, PWMs, etc.).
|
|
29
|
+
- Checking device version.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
Discovering devices in network:
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
from tinytoolslib.discovery import run_discovery_all
|
|
37
|
+
|
|
38
|
+
devices = run_discovery_all()
|
|
39
|
+
for device in devices:
|
|
40
|
+
print('{ip_address:20}{name:20}{mac_address:20}{family:10}{hardware_version:10}{software_version:15}'.format_map(device))
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Flashing firmware:
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
from tinytoolslib.flash import get_latest_firmware, Flasher
|
|
47
|
+
|
|
48
|
+
success, data = get_latest_firmware(IP_ADDRESS, USERNAME, PASSWORD, DIRECTORY_FOR_FIRMWARE_FILES)
|
|
49
|
+
if success:
|
|
50
|
+
flasher = Flasher()
|
|
51
|
+
success = flasher.run(data['path'], IP_ADDRESS, USERNAME, PASSWORD)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Working with tinycontrol devices:
|
|
55
|
+
|
|
56
|
+
```py
|
|
57
|
+
from tinytoolslib.models import get_version
|
|
58
|
+
|
|
59
|
+
version_info = get_version(IP_ADDRESS, with_device=True)
|
|
60
|
+
if version_info:
|
|
61
|
+
device_model = version_info['device_model']
|
|
62
|
+
# Get reading from device
|
|
63
|
+
device_model.get_all()
|
|
64
|
+
# Control outputs OUT
|
|
65
|
+
device_model.set_out(1, 1)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## File structure
|
|
69
|
+
|
|
70
|
+
- constants.py - constants related to tinycontrol devices.
|
|
71
|
+
- discovery.py - functions for discovering devices in network via UDP broadcast. Works for LK2.X, LK3.5 SW 1.26+, LK4.0, tcPDU.
|
|
72
|
+
- exceptions.py - errors raised in this library.
|
|
73
|
+
- flash.py - functions for updating the firmware of devices. Includes both method: TFTP and HTTP.
|
|
74
|
+
- models.py - models for working with different device types.
|
|
75
|
+
- parsers.py - functions for working with data formats used on LKs.
|
|
76
|
+
- requests.py - base request functions.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
tinytoolslib/__init__.py,sha256=nMXg9GFsk8gCcyBnh1MAeUPvo4Zcuv3sdwGHZrxfpPE,36
|
|
2
|
+
tinytoolslib/__version__.py,sha256=SXQZggCNtpYDwWAsfFLa0O_t3a-KL2cRKIMg40z4kgM,21
|
|
3
|
+
tinytoolslib/constants.py,sha256=Vednh4IGZi3Lk4-8ivdM-RPWK0nUppata8ceVsNT5ww,443
|
|
4
|
+
tinytoolslib/discovery.py,sha256=5nQNQvbk3Ywmg-uYG36CKJO0s7piUzmCvPAgnLASrj4,6028
|
|
5
|
+
tinytoolslib/exceptions.py,sha256=hm_QtkFfAGSIrLK5WAGQr-tvMMQ14vaybLHrOqM1i_s,1301
|
|
6
|
+
tinytoolslib/flash.py,sha256=OgV-Z3DiWkNxypcxN6-wwVR_fozd58EpuP4EDALZbaQ,11882
|
|
7
|
+
tinytoolslib/models.py,sha256=YfhYGO-r0N11H55zw3bPPUVEf4JzeDFJrORMaJeeA0k,60885
|
|
8
|
+
tinytoolslib/parsers.py,sha256=Buz9dNSms_2XOdLkqPQrEoqDebxNnzhAsXOOv42I9TA,2067
|
|
9
|
+
tinytoolslib/requests.py,sha256=6k58Aq3BXggkoXuqYpVk1qxdgh9jRwqFXTsP0a1skvc,7893
|
|
10
|
+
tinytoolslib-0.2.5.dist-info/METADATA,sha256=uFVo-wGG9PQBCibZnSTn_jACFo1XdmdubFOjmaBmJgM,2524
|
|
11
|
+
tinytoolslib-0.2.5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
12
|
+
tinytoolslib-0.2.5.dist-info/top_level.txt,sha256=wjuFc8N3gdsNRhOmJEbVyO_xVe4s6YVgDd1Y6v3W3fI,13
|
|
13
|
+
tinytoolslib-0.2.5.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tinytoolslib
|