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.
@@ -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))
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ tinytoolslib