pyxecm 1.5__py3-none-any.whl → 2.0.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.
Potentially problematic release.
This version of pyxecm might be problematic. Click here for more details.
- pyxecm/__init__.py +6 -2
- pyxecm/avts.py +1492 -0
- pyxecm/coreshare.py +1075 -960
- pyxecm/customizer/__init__.py +16 -4
- pyxecm/customizer/__main__.py +58 -0
- pyxecm/customizer/api/__init__.py +5 -0
- pyxecm/customizer/api/__main__.py +6 -0
- pyxecm/customizer/api/app.py +914 -0
- pyxecm/customizer/api/auth.py +154 -0
- pyxecm/customizer/api/metrics.py +92 -0
- pyxecm/customizer/api/models.py +13 -0
- pyxecm/customizer/api/payload_list.py +865 -0
- pyxecm/customizer/api/settings.py +103 -0
- pyxecm/customizer/browser_automation.py +332 -139
- pyxecm/customizer/customizer.py +1075 -1057
- pyxecm/customizer/exceptions.py +35 -0
- pyxecm/customizer/guidewire.py +322 -0
- pyxecm/customizer/k8s.py +787 -338
- pyxecm/customizer/log.py +107 -0
- pyxecm/customizer/m365.py +3424 -2270
- pyxecm/customizer/nhc.py +1169 -0
- pyxecm/customizer/openapi.py +258 -0
- pyxecm/customizer/payload.py +18201 -7030
- pyxecm/customizer/pht.py +1047 -210
- pyxecm/customizer/salesforce.py +836 -727
- pyxecm/customizer/sap.py +58 -41
- pyxecm/customizer/servicenow.py +851 -383
- pyxecm/customizer/settings.py +442 -0
- pyxecm/customizer/successfactors.py +408 -346
- pyxecm/customizer/translate.py +83 -48
- pyxecm/helper/__init__.py +5 -2
- pyxecm/helper/assoc.py +98 -38
- pyxecm/helper/data.py +2482 -742
- pyxecm/helper/logadapter.py +27 -0
- pyxecm/helper/web.py +229 -101
- pyxecm/helper/xml.py +528 -172
- pyxecm/maintenance_page/__init__.py +5 -0
- pyxecm/maintenance_page/__main__.py +6 -0
- pyxecm/maintenance_page/app.py +51 -0
- pyxecm/maintenance_page/settings.py +28 -0
- pyxecm/maintenance_page/static/favicon.avif +0 -0
- pyxecm/maintenance_page/templates/maintenance.html +165 -0
- pyxecm/otac.py +234 -140
- pyxecm/otawp.py +2689 -0
- pyxecm/otcs.py +12344 -7547
- pyxecm/otds.py +3166 -2219
- pyxecm/otiv.py +36 -21
- pyxecm/otmm.py +1363 -296
- pyxecm/otpd.py +231 -127
- pyxecm-2.0.0.dist-info/METADATA +145 -0
- pyxecm-2.0.0.dist-info/RECORD +54 -0
- {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info}/WHEEL +1 -1
- pyxecm-1.5.dist-info/METADATA +0 -51
- pyxecm-1.5.dist-info/RECORD +0 -30
- {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info/licenses}/LICENSE +0 -0
- {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Custom adapter to prefix all messages with a custom prefix."""
|
|
2
|
+
|
|
3
|
+
__author__ = "Dr. Marc Diefenbruch"
|
|
4
|
+
__copyright__ = "Copyright (C) 2024-2025, OpenText"
|
|
5
|
+
__credits__ = ["Kai-Philip Gatzweiler"]
|
|
6
|
+
__maintainer__ = "Dr. Marc Diefenbruch"
|
|
7
|
+
__email__ = "mdiefenb@opentext.com"
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PrefixLogAdapter(logging.LoggerAdapter):
|
|
13
|
+
"""Prefix all messages with a custom prefix."""
|
|
14
|
+
|
|
15
|
+
def process(self, msg: str, kwargs: dict) -> tuple[str, dict]:
|
|
16
|
+
"""TODO _summary_.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
msg (_type_): TODO _description_
|
|
20
|
+
kwargs (_type_): TODO _description_
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
_type_: _description_
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
return "[{}] {}".format(self.extra["prefix"], msg), kwargs
|
pyxecm/helper/web.py
CHANGED
|
@@ -1,74 +1,104 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Module to implement functions to execute Web Requests
|
|
3
|
-
|
|
4
|
-
Class: HTTP
|
|
5
|
-
Methods:
|
|
6
|
-
|
|
7
|
-
__init__ : class initializer
|
|
8
|
-
check_host_reachable: checks if a server / host is reachable
|
|
9
|
-
http_request: make a HTTP request to a defined URL / endpoint (e.g. a Web Hook)
|
|
10
|
-
|
|
11
|
-
"""
|
|
1
|
+
"""Module to implement functions to execute Web Requests."""
|
|
12
2
|
|
|
13
3
|
__author__ = "Dr. Marc Diefenbruch"
|
|
14
|
-
__copyright__ = "Copyright 2024, OpenText"
|
|
4
|
+
__copyright__ = "Copyright (C) 2024-2025, OpenText"
|
|
15
5
|
__credits__ = ["Kai-Philip Gatzweiler"]
|
|
16
6
|
__maintainer__ = "Dr. Marc Diefenbruch"
|
|
17
7
|
__email__ = "mdiefenb@opentext.com"
|
|
18
8
|
|
|
19
9
|
import logging
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
20
12
|
import socket
|
|
13
|
+
import sys
|
|
21
14
|
import time
|
|
15
|
+
from importlib.metadata import version
|
|
16
|
+
from urllib.parse import urlparse
|
|
17
|
+
|
|
22
18
|
import requests
|
|
23
19
|
from lxml import html
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
APP_NAME = "pyxecm"
|
|
22
|
+
APP_VERSION = version("pyxecm")
|
|
23
|
+
MODULE_NAME = APP_NAME + ".helper.web"
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
26
|
+
OS_INFO = f"{platform.system()} {platform.release()}"
|
|
27
|
+
ARCH_INFO = platform.machine()
|
|
28
|
+
REQUESTS_VERSION = requests.__version__
|
|
28
29
|
|
|
30
|
+
USER_AGENT = (
|
|
31
|
+
f"{APP_NAME}/{APP_VERSION} ({MODULE_NAME}/{APP_VERSION}; "
|
|
32
|
+
f"Python/{PYTHON_VERSION}; {OS_INFO}; {ARCH_INFO}; Requests/{REQUESTS_VERSION})"
|
|
33
|
+
)
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
""
|
|
35
|
+
REQUEST_FORM_HEADERS = {
|
|
36
|
+
"User-Agent": USER_AGENT,
|
|
37
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
38
|
+
}
|
|
39
|
+
REQUEST_TIMEOUT = 120
|
|
40
|
+
REQUEST_RETRY_DELAY = 20
|
|
41
|
+
REQUEST_MAX_RETRIES = 2
|
|
32
42
|
|
|
33
|
-
|
|
43
|
+
default_logger = logging.getLogger(MODULE_NAME)
|
|
34
44
|
|
|
35
|
-
def __init__(self):
|
|
36
|
-
"""Initialize the HTTP object
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
class HTTP:
|
|
47
|
+
"""Class HTTP is used to issue HTTP request and test if hosts are reachable."""
|
|
48
|
+
|
|
49
|
+
logger: logging.Logger = default_logger
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
logger: logging.Logger = default_logger,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Initialize the HTTP object."""
|
|
56
|
+
|
|
57
|
+
if logger != default_logger:
|
|
58
|
+
self.logger = logger.getChild("http")
|
|
59
|
+
for logfilter in logger.filters:
|
|
60
|
+
self.logger.addFilter(logfilter)
|
|
61
|
+
|
|
62
|
+
# end method definition
|
|
40
63
|
|
|
41
64
|
def check_host_reachable(self, hostname: str, port: int = 80) -> bool:
|
|
42
|
-
"""Check if a server / web address is reachable
|
|
65
|
+
"""Check if a server / web address is reachable.
|
|
43
66
|
|
|
44
67
|
Args:
|
|
45
|
-
hostname (str):
|
|
46
|
-
|
|
68
|
+
hostname (str):
|
|
69
|
+
The endpoint hostname.
|
|
70
|
+
port (int):
|
|
71
|
+
The endpoint port.
|
|
72
|
+
|
|
47
73
|
Results:
|
|
48
|
-
bool:
|
|
74
|
+
bool:
|
|
75
|
+
True is reachable, False otherwise
|
|
76
|
+
|
|
49
77
|
"""
|
|
50
78
|
|
|
51
|
-
logger.debug(
|
|
52
|
-
"Test if host -> %s is reachable on port -> %s ...",
|
|
79
|
+
self.logger.debug(
|
|
80
|
+
"Test if host -> '%s' is reachable on port -> %s ...",
|
|
81
|
+
hostname,
|
|
82
|
+
str(port),
|
|
53
83
|
)
|
|
54
84
|
try:
|
|
55
85
|
socket.getaddrinfo(hostname, port)
|
|
56
86
|
except socket.gaierror as exception:
|
|
57
|
-
logger.warning(
|
|
87
|
+
self.logger.warning(
|
|
58
88
|
"Address-related error - cannot reach host -> %s; error -> %s",
|
|
59
89
|
hostname,
|
|
60
90
|
exception.strerror,
|
|
61
91
|
)
|
|
62
92
|
return False
|
|
63
|
-
except
|
|
64
|
-
logger.warning(
|
|
93
|
+
except OSError as exception:
|
|
94
|
+
self.logger.warning(
|
|
65
95
|
"Connection error - cannot reach host -> %s; error -> %s",
|
|
66
96
|
hostname,
|
|
67
97
|
exception.strerror,
|
|
68
98
|
)
|
|
69
99
|
return False
|
|
70
100
|
else:
|
|
71
|
-
logger.debug("Host is reachable at -> %s:%s", hostname, str(port))
|
|
101
|
+
self.logger.debug("Host is reachable at -> %s:%s", hostname, str(port))
|
|
72
102
|
return True
|
|
73
103
|
|
|
74
104
|
# end method definition
|
|
@@ -79,52 +109,72 @@ class HTTP(object):
|
|
|
79
109
|
method: str = "POST",
|
|
80
110
|
payload: dict | None = None,
|
|
81
111
|
headers: dict | None = None,
|
|
82
|
-
timeout: int =
|
|
83
|
-
retries: int =
|
|
84
|
-
wait_time: int =
|
|
112
|
+
timeout: int = REQUEST_TIMEOUT,
|
|
113
|
+
retries: int = REQUEST_MAX_RETRIES,
|
|
114
|
+
wait_time: int = REQUEST_RETRY_DELAY,
|
|
85
115
|
wait_on_status: list | None = None,
|
|
86
116
|
show_error: bool = True,
|
|
87
|
-
|
|
117
|
+
stream: bool = False,
|
|
118
|
+
) -> dict | None:
|
|
88
119
|
"""Issues an http request to a given URL.
|
|
89
120
|
|
|
90
121
|
Args:
|
|
91
|
-
url (str):
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
122
|
+
url (str):
|
|
123
|
+
The URL of the request.
|
|
124
|
+
method (str, optional):
|
|
125
|
+
Method of the request (POST, PUT, GET, ...). Defaults to "POST".
|
|
126
|
+
payload (dict, optional):
|
|
127
|
+
Request payload. Defaults to None.
|
|
128
|
+
headers (dict, optional):
|
|
129
|
+
Request header. Defaults to None. If None then a default
|
|
130
|
+
value defined in REQUEST_FORM_HEADERS is used.
|
|
131
|
+
timeout (int, optional):
|
|
132
|
+
The timeout in seconds. Defaults to REQUEST_TIMEOUT.
|
|
133
|
+
retries (int, optional):
|
|
134
|
+
The number of retries. If -1 then unlimited retries.
|
|
135
|
+
Defaults to REQUEST_MAX_RETRIES.
|
|
136
|
+
wait_time (int, optional):
|
|
137
|
+
The number of seconds to wait after each try.
|
|
138
|
+
Defaults to REQUEST_RETRY_DELAY.
|
|
139
|
+
wait_on_status (list, optional):
|
|
140
|
+
A list of status codes we want to wait on.
|
|
141
|
+
If None or empty then we wait for all return codes if
|
|
142
|
+
wait_time > 0.
|
|
143
|
+
show_error (bool, optional):
|
|
144
|
+
Whether to show an error or a warning message in case of an error.
|
|
145
|
+
stream (bool, optional):
|
|
146
|
+
Enable stream for response content (e.g. for downloading large files).
|
|
147
|
+
|
|
102
148
|
Returns:
|
|
103
|
-
|
|
149
|
+
dict | None:
|
|
150
|
+
Response of call
|
|
151
|
+
|
|
104
152
|
"""
|
|
105
153
|
|
|
106
154
|
if not headers:
|
|
107
|
-
headers =
|
|
155
|
+
headers = REQUEST_FORM_HEADERS
|
|
108
156
|
|
|
109
|
-
message = "Make HTTP
|
|
110
|
-
url,
|
|
157
|
+
message = "Make HTTP request to URL -> '{}' using -> {} method".format(
|
|
158
|
+
url,
|
|
159
|
+
method,
|
|
111
160
|
)
|
|
112
161
|
if payload:
|
|
113
162
|
message += " with payload -> {}".format(payload)
|
|
114
163
|
if retries:
|
|
115
164
|
message += " (max number of retries -> {}, wait time between retries -> {})".format(
|
|
116
|
-
retries,
|
|
165
|
+
retries,
|
|
166
|
+
wait_time,
|
|
117
167
|
)
|
|
118
168
|
try:
|
|
119
169
|
retries = int(retries)
|
|
120
170
|
except ValueError:
|
|
121
|
-
logger.warning(
|
|
171
|
+
self.logger.warning(
|
|
122
172
|
"HTTP request -> retries is not a valid integer value: %s, defaulting to 0 retries ",
|
|
123
173
|
retries,
|
|
124
174
|
)
|
|
125
175
|
retries = 0
|
|
126
176
|
|
|
127
|
-
logger.debug(message)
|
|
177
|
+
self.logger.debug(message)
|
|
128
178
|
|
|
129
179
|
try_counter = 1
|
|
130
180
|
|
|
@@ -136,23 +186,22 @@ class HTTP(object):
|
|
|
136
186
|
data=payload,
|
|
137
187
|
headers=headers,
|
|
138
188
|
timeout=timeout,
|
|
189
|
+
stream=stream,
|
|
139
190
|
)
|
|
140
|
-
|
|
141
|
-
except Exception as exc:
|
|
191
|
+
except requests.RequestException as exc:
|
|
142
192
|
response = None
|
|
143
|
-
logger.warning(
|
|
144
|
-
"HTTP request -> %s to url -> %s failed
|
|
193
|
+
self.logger.warning(
|
|
194
|
+
"HTTP request -> %s to url -> %s failed (try %s); error -> %s",
|
|
145
195
|
method,
|
|
146
196
|
url,
|
|
147
197
|
try_counter,
|
|
148
198
|
exc,
|
|
149
199
|
)
|
|
150
200
|
|
|
151
|
-
# do we have an error and don't want to retry?
|
|
152
201
|
if response is not None:
|
|
153
202
|
# Do we have a successful result?
|
|
154
203
|
if response.ok:
|
|
155
|
-
logger.debug(
|
|
204
|
+
self.logger.debug(
|
|
156
205
|
"HTTP request -> %s to url -> %s succeeded with status -> %s!",
|
|
157
206
|
method,
|
|
158
207
|
url,
|
|
@@ -160,44 +209,45 @@ class HTTP(object):
|
|
|
160
209
|
)
|
|
161
210
|
|
|
162
211
|
if wait_on_status and response.status_code in wait_on_status:
|
|
163
|
-
logger.debug(
|
|
164
|
-
"%s is in wait_on_status list
|
|
212
|
+
self.logger.debug(
|
|
213
|
+
"%s is in wait_on_status list -> %s",
|
|
165
214
|
response.status_code,
|
|
166
215
|
wait_on_status,
|
|
167
216
|
)
|
|
168
217
|
else:
|
|
169
218
|
return response
|
|
170
219
|
|
|
171
|
-
|
|
220
|
+
else:
|
|
172
221
|
message = "HTTP request -> {} to url -> {} failed; status -> {}; error -> {}".format(
|
|
173
222
|
method,
|
|
174
223
|
url,
|
|
175
224
|
response.status_code,
|
|
176
225
|
(
|
|
177
226
|
response.text
|
|
178
|
-
if response.headers.get("content-type")
|
|
179
|
-
== "application/json"
|
|
227
|
+
if response.headers.get("content-type") == "application/json"
|
|
180
228
|
else "see debug log"
|
|
181
229
|
),
|
|
182
230
|
)
|
|
183
231
|
if show_error and retries == 0:
|
|
184
|
-
logger.error(message)
|
|
232
|
+
self.logger.error(message)
|
|
185
233
|
else:
|
|
186
|
-
logger.warning(message)
|
|
234
|
+
self.logger.warning(message)
|
|
235
|
+
# end if response is not None
|
|
187
236
|
|
|
188
237
|
# Check if another retry is allowed, if not return None
|
|
189
238
|
if retries == 0:
|
|
190
239
|
return None
|
|
191
240
|
|
|
192
241
|
if wait_time > 0:
|
|
193
|
-
logger.warning(
|
|
242
|
+
self.logger.warning(
|
|
194
243
|
"Sleeping %s seconds and then trying once more...",
|
|
195
|
-
str(wait_time),
|
|
244
|
+
str(wait_time * try_counter),
|
|
196
245
|
)
|
|
197
|
-
time.sleep(wait_time)
|
|
246
|
+
time.sleep(wait_time * try_counter)
|
|
198
247
|
|
|
199
248
|
retries -= 1
|
|
200
249
|
try_counter += 1
|
|
250
|
+
# end while True:
|
|
201
251
|
|
|
202
252
|
# end method definition
|
|
203
253
|
|
|
@@ -205,28 +255,48 @@ class HTTP(object):
|
|
|
205
255
|
self,
|
|
206
256
|
url: str,
|
|
207
257
|
filename: str,
|
|
208
|
-
timeout: int =
|
|
209
|
-
retries: int =
|
|
210
|
-
wait_time: int =
|
|
258
|
+
timeout: int = REQUEST_TIMEOUT,
|
|
259
|
+
retries: int = REQUEST_MAX_RETRIES,
|
|
260
|
+
wait_time: int = REQUEST_RETRY_DELAY,
|
|
211
261
|
wait_on_status: list | None = None,
|
|
262
|
+
chunk_size: int = 8192,
|
|
212
263
|
show_error: bool = True,
|
|
213
264
|
) -> bool:
|
|
214
|
-
"""Download a file from a URL
|
|
265
|
+
"""Download a file from a URL.
|
|
215
266
|
|
|
216
267
|
Args:
|
|
217
|
-
url (str):
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
268
|
+
url (str):
|
|
269
|
+
The URL to open / load.
|
|
270
|
+
filename (str):
|
|
271
|
+
The filename to save the content.
|
|
272
|
+
timeout (int, optional):
|
|
273
|
+
The timeout in seconds.
|
|
274
|
+
retries (int, optional):
|
|
275
|
+
The number of retries. If -1 then unlimited retries.
|
|
276
|
+
wait_time (int, optional):
|
|
277
|
+
The number of seconds to wait after each try.
|
|
278
|
+
wait_on_status (list, optional):
|
|
279
|
+
The list of status codes we want to wait on.
|
|
280
|
+
If None or empty then we wait for all return codes if
|
|
281
|
+
wait_time > 0.
|
|
282
|
+
chunk_size (int, optional):
|
|
283
|
+
Chunk size for reading file content. Default is 8192.
|
|
284
|
+
show_error (bool, optional):
|
|
285
|
+
Whether or not an error show logged if download fails.
|
|
286
|
+
Default is True.
|
|
225
287
|
|
|
226
288
|
Returns:
|
|
227
|
-
bool:
|
|
289
|
+
bool:
|
|
290
|
+
True if successful, False otherwise.
|
|
291
|
+
|
|
228
292
|
"""
|
|
229
293
|
|
|
294
|
+
# Validate the URL:
|
|
295
|
+
parsed_url = urlparse(url)
|
|
296
|
+
if not parsed_url.scheme or not parsed_url.netloc:
|
|
297
|
+
self.logger.error("Invalid URL -> '%s' to download a file!", url)
|
|
298
|
+
return False
|
|
299
|
+
|
|
230
300
|
response = self.http_request(
|
|
231
301
|
url=url,
|
|
232
302
|
method="GET",
|
|
@@ -235,38 +305,91 @@ class HTTP(object):
|
|
|
235
305
|
wait_time=wait_time,
|
|
236
306
|
wait_on_status=wait_on_status,
|
|
237
307
|
show_error=show_error,
|
|
308
|
+
stream=True, # for downloads we want streaming
|
|
238
309
|
)
|
|
239
310
|
|
|
240
|
-
if response
|
|
311
|
+
if not response or not response.ok:
|
|
312
|
+
self.logger.error(
|
|
313
|
+
"Failed to request download file -> '%s' from site -> %s%s",
|
|
314
|
+
filename,
|
|
315
|
+
url,
|
|
316
|
+
"; error -> {}".format(response.text) if response else "",
|
|
317
|
+
)
|
|
241
318
|
return False
|
|
242
319
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
320
|
+
try:
|
|
321
|
+
directory = os.path.dirname(filename)
|
|
322
|
+
if not os.path.exists(directory):
|
|
323
|
+
self.logger.info(
|
|
324
|
+
"Download directory -> '%s' does not exist, creating it.",
|
|
325
|
+
directory,
|
|
326
|
+
)
|
|
327
|
+
os.makedirs(directory)
|
|
328
|
+
with open(filename, "wb") as download_file:
|
|
329
|
+
for chunk in response.iter_content(chunk_size=chunk_size):
|
|
330
|
+
download_file.write(chunk)
|
|
331
|
+
self.logger.debug(
|
|
332
|
+
"File downloaded successfully as -> '%s' (size -> %s).",
|
|
333
|
+
filename,
|
|
334
|
+
self.human_readable_size(os.path.getsize(filename)),
|
|
335
|
+
)
|
|
336
|
+
except (OSError, requests.exceptions.RequestException):
|
|
337
|
+
self.logger.error(
|
|
338
|
+
"Cannot write content to file -> '%s' in directory -> '%s'!",
|
|
339
|
+
filename,
|
|
340
|
+
directory,
|
|
341
|
+
)
|
|
342
|
+
return False
|
|
343
|
+
else:
|
|
247
344
|
return True
|
|
248
345
|
|
|
249
|
-
|
|
346
|
+
# end method definition
|
|
347
|
+
|
|
348
|
+
def human_readable_size(self, size_in_bytes: int) -> str:
|
|
349
|
+
"""Return a file size in human readable form.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
size_in_bytes (int): The file size in bytes.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
str:
|
|
356
|
+
The formatted size using units.
|
|
357
|
+
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
361
|
+
if size_in_bytes < 1024:
|
|
362
|
+
return "{:.2f} {}".format(size_in_bytes, unit)
|
|
363
|
+
size_in_bytes /= 1024
|
|
364
|
+
|
|
365
|
+
# We should never get here but linter wants it:
|
|
366
|
+
return "{:.2f}".format(size_in_bytes)
|
|
250
367
|
|
|
251
368
|
# end method definition
|
|
252
369
|
|
|
253
370
|
def extract_content(self, url: str, xpath: str) -> str | None:
|
|
254
|
-
"""Extract a string from a response of a HTTP request
|
|
255
|
-
based on an XPath.
|
|
371
|
+
"""Extract a string from a response of a HTTP request based on an XPath.
|
|
256
372
|
|
|
257
373
|
Args:
|
|
258
|
-
url (str):
|
|
259
|
-
|
|
374
|
+
url (str):
|
|
375
|
+
The URL to open / load.
|
|
376
|
+
xpath (str):
|
|
377
|
+
The XPath expression to apply to the result.
|
|
260
378
|
|
|
261
379
|
Returns:
|
|
262
|
-
str | None:
|
|
380
|
+
str | None:
|
|
381
|
+
Extracted string or None in case of an error.
|
|
382
|
+
|
|
263
383
|
"""
|
|
264
384
|
|
|
265
|
-
# Send a GET request to the URL
|
|
266
|
-
response =
|
|
385
|
+
# Send a GET request to the URL:
|
|
386
|
+
response = self.http_request(
|
|
387
|
+
url=url,
|
|
388
|
+
method="GET",
|
|
389
|
+
)
|
|
267
390
|
|
|
268
391
|
# Check if request was successful
|
|
269
|
-
if response
|
|
392
|
+
if response and response.ok:
|
|
270
393
|
# Parse the HTML content
|
|
271
394
|
tree = html.fromstring(response.content)
|
|
272
395
|
|
|
@@ -276,9 +399,14 @@ class HTTP(object):
|
|
|
276
399
|
# Get text content of all elements and join them
|
|
277
400
|
content = "\n".join([elem.text_content().strip() for elem in elements])
|
|
278
401
|
|
|
279
|
-
# Return the extracted content
|
|
402
|
+
# Return the extracted content:
|
|
280
403
|
return content
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
404
|
+
|
|
405
|
+
# If request was not successful, print error message:
|
|
406
|
+
self.logger.error(
|
|
407
|
+
"Cannot extract content from URL -> '%s'; error code -> %s",
|
|
408
|
+
url,
|
|
409
|
+
response.status_code,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return None
|