pyxecm 1.6__py3-none-any.whl → 2.0.1__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.

Files changed (78) hide show
  1. pyxecm/__init__.py +7 -4
  2. pyxecm/avts.py +727 -254
  3. pyxecm/coreshare.py +686 -467
  4. pyxecm/customizer/__init__.py +16 -4
  5. pyxecm/customizer/__main__.py +58 -0
  6. pyxecm/customizer/api/__init__.py +5 -0
  7. pyxecm/customizer/api/__main__.py +6 -0
  8. pyxecm/customizer/api/app.py +163 -0
  9. pyxecm/customizer/api/auth/__init__.py +1 -0
  10. pyxecm/customizer/api/auth/functions.py +92 -0
  11. pyxecm/customizer/api/auth/models.py +13 -0
  12. pyxecm/customizer/api/auth/router.py +78 -0
  13. pyxecm/customizer/api/common/__init__.py +1 -0
  14. pyxecm/customizer/api/common/functions.py +47 -0
  15. pyxecm/customizer/api/common/metrics.py +92 -0
  16. pyxecm/customizer/api/common/models.py +21 -0
  17. pyxecm/customizer/api/common/payload_list.py +870 -0
  18. pyxecm/customizer/api/common/router.py +72 -0
  19. pyxecm/customizer/api/settings.py +128 -0
  20. pyxecm/customizer/api/terminal/__init__.py +1 -0
  21. pyxecm/customizer/api/terminal/router.py +87 -0
  22. pyxecm/customizer/api/v1_csai/__init__.py +1 -0
  23. pyxecm/customizer/api/v1_csai/router.py +87 -0
  24. pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
  25. pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
  26. pyxecm/customizer/api/v1_maintenance/models.py +12 -0
  27. pyxecm/customizer/api/v1_maintenance/router.py +76 -0
  28. pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
  29. pyxecm/customizer/api/v1_otcs/functions.py +61 -0
  30. pyxecm/customizer/api/v1_otcs/router.py +179 -0
  31. pyxecm/customizer/api/v1_payload/__init__.py +1 -0
  32. pyxecm/customizer/api/v1_payload/functions.py +179 -0
  33. pyxecm/customizer/api/v1_payload/models.py +51 -0
  34. pyxecm/customizer/api/v1_payload/router.py +499 -0
  35. pyxecm/customizer/browser_automation.py +721 -286
  36. pyxecm/customizer/customizer.py +1076 -1425
  37. pyxecm/customizer/exceptions.py +35 -0
  38. pyxecm/customizer/guidewire.py +1186 -0
  39. pyxecm/customizer/k8s.py +901 -379
  40. pyxecm/customizer/log.py +107 -0
  41. pyxecm/customizer/m365.py +2967 -920
  42. pyxecm/customizer/nhc.py +1169 -0
  43. pyxecm/customizer/openapi.py +258 -0
  44. pyxecm/customizer/payload.py +18228 -7820
  45. pyxecm/customizer/pht.py +717 -286
  46. pyxecm/customizer/salesforce.py +516 -342
  47. pyxecm/customizer/sap.py +58 -41
  48. pyxecm/customizer/servicenow.py +611 -372
  49. pyxecm/customizer/settings.py +445 -0
  50. pyxecm/customizer/successfactors.py +408 -346
  51. pyxecm/customizer/translate.py +83 -48
  52. pyxecm/helper/__init__.py +5 -2
  53. pyxecm/helper/assoc.py +83 -43
  54. pyxecm/helper/data.py +2406 -870
  55. pyxecm/helper/logadapter.py +27 -0
  56. pyxecm/helper/web.py +229 -101
  57. pyxecm/helper/xml.py +596 -171
  58. pyxecm/maintenance_page/__init__.py +5 -0
  59. pyxecm/maintenance_page/__main__.py +6 -0
  60. pyxecm/maintenance_page/app.py +51 -0
  61. pyxecm/maintenance_page/settings.py +28 -0
  62. pyxecm/maintenance_page/static/favicon.avif +0 -0
  63. pyxecm/maintenance_page/templates/maintenance.html +165 -0
  64. pyxecm/otac.py +235 -141
  65. pyxecm/otawp.py +2668 -1220
  66. pyxecm/otca.py +569 -0
  67. pyxecm/otcs.py +7956 -3237
  68. pyxecm/otds.py +2178 -925
  69. pyxecm/otiv.py +36 -21
  70. pyxecm/otmm.py +1272 -325
  71. pyxecm/otpd.py +231 -127
  72. pyxecm-2.0.1.dist-info/METADATA +122 -0
  73. pyxecm-2.0.1.dist-info/RECORD +76 -0
  74. {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info}/WHEEL +1 -1
  75. pyxecm-1.6.dist-info/METADATA +0 -53
  76. pyxecm-1.6.dist-info/RECORD +0 -32
  77. {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info/licenses}/LICENSE +0 -0
  78. {pyxecm-1.6.dist-info → pyxecm-2.0.1.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
- logger = logging.getLogger("pyxecm.web")
21
+ APP_NAME = "pyxecm"
22
+ APP_VERSION = version("pyxecm")
23
+ MODULE_NAME = APP_NAME + ".helper.web"
26
24
 
27
- requestHeaders = {"Content-Type": "application/x-www-form-urlencoded"}
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
- class HTTP(object):
31
- """Used to issue HTTP request and test if hosts are reachable."""
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
- _config = None
43
+ default_logger = logging.getLogger(MODULE_NAME)
34
44
 
35
- def __init__(self):
36
- """Initialize the HTTP object
37
45
 
38
- Args:
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): endpoint hostname
46
- port (int): endpoint port
68
+ hostname (str):
69
+ The endpoint hostname.
70
+ port (int):
71
+ The endpoint port.
72
+
47
73
  Results:
48
- bool: True is reachable, False otherwise
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 ...", hostname, str(port)
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 socket.error as exception:
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 = 60,
83
- retries: int = 0,
84
- wait_time: int = 0,
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): URL of the request
92
- method (str, optional): Method of the request (POST, PUT, GET, ...). Defaults to "POST".
93
- payload (dict, optional): Request payload. Defaults to None.
94
- headers (dict, optional): Request header. Defaults to None. If None then a default
95
- value defined in "requestHeaders" is used.
96
- timeout (int, optional): timeout in seconds
97
- retries (int, optional): number of retries. If -1 then unlimited retries.
98
- wait_time (int, optional): number of seconds to wait after each try
99
- wait_on_status (list, optional): list of status codes we want to wait on. If None
100
- or empty then we wait for all return codes if
101
- wait_time > 0
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
- Response of call
149
+ dict | None:
150
+ Response of call
151
+
104
152
  """
105
153
 
106
154
  if not headers:
107
- headers = requestHeaders
155
+ headers = REQUEST_FORM_HEADERS
108
156
 
109
- message = "Make HTTP Request to URL -> {} using -> {} method".format(
110
- url, method
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, wait_time
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
- logger.debug("%s", response.text)
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 failed (try %s); error -> %s",
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: %s",
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
- elif not response.ok:
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 = 120,
209
- retries: int = 0,
210
- wait_time: int = 0,
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): URL
218
- filename (str): filename to save
219
- timeout (int, optional): timeout in seconds
220
- retries (int, optional): number of retries. If -1 then unlimited retries.
221
- wait_time (int, optional): number of seconds to wait after each try
222
- wait_on_status (list, optional): list of status codes we want to wait on. If None
223
- or empty then we wait for all return codes if
224
- wait_time > 0
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: True if successful, False otherwise
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 is None:
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
- if response.ok:
244
- with open(filename, "wb") as f:
245
- f.write(response.content)
246
- logger.debug("File downloaded successfully as -> %s", filename)
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
- return False
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): URL to open
259
- xpath (str): XPath expression to apply to the result
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: Extracted string or None in case of an error.
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 = requests.get(url, timeout=None)
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.status_code == 200:
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
- else:
282
- # If request was not successful, print error message
283
- logger.error(response.status_code)
284
- return None
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