scriptcollection 4.2.81__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.
Files changed (62) hide show
  1. ScriptCollection/AnionBuildPlatform.py +199 -0
  2. ScriptCollection/CertificateUpdater.py +149 -0
  3. ScriptCollection/Executables.py +921 -0
  4. ScriptCollection/GeneralUtilities.py +1589 -0
  5. ScriptCollection/HTTPMaintenanceOverheadHelper.py +36 -0
  6. ScriptCollection/OCIImages/AbstractImageHandler.py +38 -0
  7. ScriptCollection/OCIImages/ConcreteImageHandlers/ImageHandlerDebian.py +20 -0
  8. ScriptCollection/OCIImages/ConcreteImageHandlers/ImageHandlerDebianSlim.py +20 -0
  9. ScriptCollection/OCIImages/ConcreteImageHandlers/ImageHandlerGeneric.py +20 -0
  10. ScriptCollection/OCIImages/ConcreteImageHandlers/ImageHandlerGenericV.py +20 -0
  11. ScriptCollection/OCIImages/ConcreteImageHandlers/ImageHandlerGitlabCE.py +20 -0
  12. ScriptCollection/OCIImages/ConcreteImageHandlers/ImageHandlerGitlabEE.py +20 -0
  13. ScriptCollection/OCIImages/ConcreteImageHandlers/__init__.py +0 -0
  14. ScriptCollection/OCIImages/OCIImageManager.py +190 -0
  15. ScriptCollection/OCIImages/__init__.py +0 -0
  16. ScriptCollection/ProcessesRunner.py +43 -0
  17. ScriptCollection/ProgramRunnerBase.py +47 -0
  18. ScriptCollection/ProgramRunnerMock.py +2 -0
  19. ScriptCollection/ProgramRunnerPopen.py +57 -0
  20. ScriptCollection/ProgramRunnerSudo.py +108 -0
  21. ScriptCollection/Resources/CultureChooser/CultureChooser.js +29 -0
  22. ScriptCollection/Resources/CultureChooser/index.html +15 -0
  23. ScriptCollection/Resources/MaintenanceSite/MaintenanceSite.html +15 -0
  24. ScriptCollection/SCLog.py +115 -0
  25. ScriptCollection/ScriptCollectionCore.py +3485 -0
  26. ScriptCollection/TFCPS/Docker/TFCPS_CodeUnitSpecific_Docker.py +192 -0
  27. ScriptCollection/TFCPS/Docker/__init__.py +0 -0
  28. ScriptCollection/TFCPS/DotNet/CertificateGeneratorInformationBase.py +8 -0
  29. ScriptCollection/TFCPS/DotNet/CertificateGeneratorInformationGenerate.py +6 -0
  30. ScriptCollection/TFCPS/DotNet/CertificateGeneratorInformationNoGenerate.py +7 -0
  31. ScriptCollection/TFCPS/DotNet/TFCPS_CodeUnitSpecific_DotNet.py +547 -0
  32. ScriptCollection/TFCPS/DotNet/__init__.py +0 -0
  33. ScriptCollection/TFCPS/Flutter/TFCPS_CodeUnitSpecific_Flutter.py +137 -0
  34. ScriptCollection/TFCPS/Flutter/__init__.py +0 -0
  35. ScriptCollection/TFCPS/Go/TFCPS_CodeUnitSpecific_Go.py +72 -0
  36. ScriptCollection/TFCPS/Go/__init__.py +0 -0
  37. ScriptCollection/TFCPS/Maven/TFCPS_CodeUnitSpecific_Maven.py +42 -0
  38. ScriptCollection/TFCPS/Maven/__init__.py +0 -0
  39. ScriptCollection/TFCPS/NodeJS/TFCPS_CodeUnitSpecific_NodeJS.py +232 -0
  40. ScriptCollection/TFCPS/NodeJS/__init__.py +0 -0
  41. ScriptCollection/TFCPS/Python/TFCPS_CodeUnitSpecific_Python.py +239 -0
  42. ScriptCollection/TFCPS/Python/__init__.py +0 -0
  43. ScriptCollection/TFCPS/Rust/TFCPS_CodeUnitSpecific_Rust.py +42 -0
  44. ScriptCollection/TFCPS/Rust/__init__.py +0 -0
  45. ScriptCollection/TFCPS/TFCPS_CodeUnitSpecific_Base.py +433 -0
  46. ScriptCollection/TFCPS/TFCPS_CodeUnit_BuildCodeUnit.py +135 -0
  47. ScriptCollection/TFCPS/TFCPS_CodeUnit_BuildCodeUnits.py +301 -0
  48. ScriptCollection/TFCPS/TFCPS_CreateRelease.py +98 -0
  49. ScriptCollection/TFCPS/TFCPS_Generic.py +44 -0
  50. ScriptCollection/TFCPS/TFCPS_MergeToMain.py +128 -0
  51. ScriptCollection/TFCPS/TFCPS_MergeToStable.py +356 -0
  52. ScriptCollection/TFCPS/TFCPS_PreBuildCodeunitsScript.py +48 -0
  53. ScriptCollection/TFCPS/TFCPS_Tools_General.py +1565 -0
  54. ScriptCollection/TFCPS/__init__.py +0 -0
  55. ScriptCollection/__init__.py +0 -0
  56. ScriptCollection/__pycache__/GeneralUtilities.cpython-311.pyc +0 -0
  57. ScriptCollection/__pycache__/__init__.cpython-311.pyc +0 -0
  58. scriptcollection-4.2.81.dist-info/METADATA +169 -0
  59. scriptcollection-4.2.81.dist-info/RECORD +62 -0
  60. scriptcollection-4.2.81.dist-info/WHEEL +5 -0
  61. scriptcollection-4.2.81.dist-info/entry_points.txt +67 -0
  62. scriptcollection-4.2.81.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1589 @@
1
+ import re
2
+ import os
3
+ from os import listdir
4
+ from os.path import isfile, join, isdir
5
+ import platform
6
+ import json
7
+ import inspect
8
+ import ctypes
9
+ import hashlib
10
+ import subprocess
11
+ import shutil
12
+ import time
13
+ import urllib
14
+ import stat
15
+ import fnmatch
16
+ import secrets
17
+ import string as strin
18
+ import sys
19
+ from importlib import resources
20
+ from enum import Enum
21
+ import traceback
22
+ import warnings
23
+ import functools
24
+ from pathlib import Path
25
+ from datetime import datetime, timedelta, date, timezone
26
+ import typing
27
+ from packaging.version import Version
28
+ import psutil
29
+ from defusedxml.minidom import parse
30
+ from OpenSSL import crypto
31
+
32
+ class VersionEcholon(Enum):
33
+ LatestPatch = 0
34
+ LatestPatchOrLatestMinor = 1
35
+ LatestPatchOrLatestMinorOrNextMajor = 2
36
+ LatestVersion = 3
37
+ CustomAlgorithm = 4
38
+
39
+ class Platform(Enum):
40
+ Windows_AMD64 = 0
41
+ Linux_AMD64 = 1
42
+ Linux_ARM64 = 2
43
+ MacOS_ARM64 = 3
44
+
45
+
46
+ class Dependency:
47
+ dependencyname:str
48
+ current_version:str
49
+
50
+ def __init__(self,dependencyname:str,current_version:str):
51
+ self.dependencyname=dependencyname
52
+ self.current_version=current_version
53
+
54
+ class GeneralUtilities:
55
+
56
+ empty_string: str = ""
57
+
58
+ @staticmethod
59
+ def get_modest_dark_url() -> str:
60
+ return "https://aniondev.github.io/CDN/ScriptCollectionDesigns/ModestDark/Style.css"
61
+
62
+ @staticmethod
63
+ def is_generic(t: typing.Type):
64
+ return hasattr(t, "__origin__")
65
+
66
+ @staticmethod
67
+ def is_debugger_attached():
68
+ return sys.gettrace() is not None
69
+
70
+ @staticmethod
71
+ def check_arguments(function):
72
+ def __check_function(*args, **named_args):
73
+ parameters: list = inspect.getfullargspec(function)[0].copy()
74
+ arguments: list = list(tuple(args)).copy()
75
+ if "self" in parameters:
76
+ parameters.remove("self")
77
+ arguments.pop(0)
78
+ for index, argument in enumerate(arguments):
79
+ if argument is not None: # Check type of None is not possible. None is always a valid argument-value
80
+ if parameters[index] in function.__annotations__: # Check if a type-hint for parameter exist. If not, no parameter-type available for argument-type-check
81
+ # Check type of arguments if the type is a generic type seems to be impossible.
82
+ if not GeneralUtilities.is_generic(function.__annotations__[parameters[index]]):
83
+ if not isinstance(argument, function.__annotations__[parameters[index]]):
84
+ raise TypeError(f"Argument with index {index} for function {function.__name__} ('{str(argument)}') is not of type {function.__annotations__[parameters[index]]} but has type "+str(type(argument)))
85
+ for index, named_argument in enumerate(named_args):
86
+ if named_args[named_argument] is not None:
87
+ if parameters[index] in function.__annotations__:
88
+ if not GeneralUtilities.is_generic(function.__annotations__.get(named_argument)):
89
+ if not isinstance(named_args[named_argument], function.__annotations__.get(named_argument)):
90
+ raise TypeError(f"Argument with name {named_argument} for function {function.__name__} ('{str(named_args[named_argument])}') is not of type {function.__annotations__.get(named_argument)}")
91
+ return function(*args, **named_args)
92
+ __check_function.__doc__ = function.__doc__
93
+ return __check_function
94
+
95
+ @staticmethod
96
+ def deprecated(reason: str=None):
97
+ def decorator(func):
98
+ @functools.wraps(func)
99
+ def wrapper(*args, **kwargs):
100
+ msg = f"Function {func.__name__}() is deprecated."
101
+ if GeneralUtilities.string_has_content(reason):
102
+ msg += f" {reason}"
103
+ warnings.warn(
104
+ msg,
105
+ category=DeprecationWarning,
106
+ stacklevel=2
107
+ )
108
+ return func(*args, **kwargs)
109
+ return wrapper
110
+ return decorator
111
+
112
+ @staticmethod
113
+ @check_arguments
114
+ def args_array_surround_with_quotes_if_required(arguments: list[str]) -> list[str]:
115
+ result = []
116
+ for argument in arguments:
117
+ if " " in argument and not (argument.startswith('"') and argument.endswith('"')):
118
+ result.append(f'"{argument}"')
119
+ else:
120
+ result.append(argument)
121
+ return result
122
+
123
+ @staticmethod
124
+ @check_arguments
125
+ def string_to_lines(string: str, add_empty_lines: bool = True, adapt_lines: bool = True) -> list[str]:
126
+ result : list[str] = list[str]()
127
+ lines : list[str] = list[str]()
128
+ if (string is not None):
129
+ if ("\n" in string):
130
+ lines = string.split("\n")
131
+ else:
132
+ lines.append(string)
133
+ for rawline in lines:
134
+ if adapt_lines:
135
+ line = rawline.replace("\r", "").strip()
136
+ else:
137
+ line = rawline
138
+ if GeneralUtilities.string_is_none_or_whitespace(line):
139
+ if add_empty_lines:
140
+ result.append(line)
141
+ else:
142
+ result.append(line)
143
+ return result
144
+
145
+ @staticmethod
146
+ @check_arguments
147
+ def string_to_datetime(value: str) -> datetime:
148
+ """expects a value in the format 2022-10-06T19:26:01"""
149
+ if "." in value:
150
+ value = value.split(".")[0]
151
+ return datetime.strptime(value,"%Y-%m-%dT%H:%M:%S")
152
+
153
+ @staticmethod
154
+ @check_arguments
155
+ def datetime_to_string(value: datetime) -> str:
156
+ """returns a value in the format 2022-10-06T19:26:01"""
157
+ return value.strftime("%Y-%m-%dT%H:%M:%S")
158
+
159
+ @staticmethod
160
+ @check_arguments
161
+ def string_to_date(value: str) -> date:
162
+ """expects a value in the format 2022-10-06"""
163
+ splitted = value.split("-")
164
+ return date(int(splitted[0]), int(splitted[1]), int(splitted[2]))
165
+
166
+ @staticmethod
167
+ @check_arguments
168
+ def date_to_string(value: date) -> str:
169
+ """returns a value in the format 2022-10-06"""
170
+ return value.strftime("%Y-%m-%d")
171
+
172
+ @staticmethod
173
+ @check_arguments
174
+ def copy_content_of_folder(source_directory: str, target_directory: str, overwrite_existing_files=False, ignored_glob_patterms: list[str] = None) -> None:
175
+ GeneralUtilities.__copy_or_move_content_of_folder(source_directory, target_directory, overwrite_existing_files, False, ignored_glob_patterms)
176
+
177
+ @staticmethod
178
+ @check_arguments
179
+ def move_content_of_folder(source_directory: str, target_directory: str, overwrite_existing_files=False, ignored_glob_patterms: list[str] = None) -> None:
180
+ GeneralUtilities.__copy_or_move_content_of_folder(source_directory, target_directory, overwrite_existing_files, True, ignored_glob_patterms)
181
+
182
+ @staticmethod
183
+ @check_arguments
184
+ def merge_dependency_lists(versions:list[list[Dependency]]) -> dict[str,set[str]]:
185
+ result:dict[str,set[str]]=dict[str,set[str]]()
186
+ for dlist in versions:
187
+ for ditem in dlist:
188
+ if not ditem.dependencyname in result:
189
+ result[ditem.dependencyname]=set[str]()
190
+ result[ditem.dependencyname].add(ditem.current_version)
191
+ return result
192
+
193
+ @staticmethod
194
+ @check_arguments
195
+ def choose_version(available_versions:list[str],current_version:str,echolon:VersionEcholon) -> str:
196
+ match echolon:
197
+ case VersionEcholon.LatestPatch:
198
+ return GeneralUtilities.get_latest_version(GeneralUtilities.filter_versions_by_prefix(available_versions,f"{GeneralUtilities.get_major_part_of_version(current_version)}.{GeneralUtilities.get_minor_part_of_version(current_version)}."))
199
+ case VersionEcholon.LatestPatchOrLatestMinor:
200
+ return GeneralUtilities.get_latest_version(GeneralUtilities.filter_versions_by_prefix(available_versions,f"{GeneralUtilities.get_major_part_of_version(current_version)}."))
201
+ case VersionEcholon.LatestPatchOrLatestMinorOrNextMajor:
202
+ raise ValueError("not implemented")#TODO
203
+ case VersionEcholon.LatestVersion:
204
+ return GeneralUtilities.get_latest_version(available_versions)
205
+ case _:
206
+ raise ValueError("Unknown echolon-value: "+str(echolon))
207
+
208
+ @staticmethod
209
+ @check_arguments
210
+ def get_latest_version(versions:list[str]) -> str:
211
+ GeneralUtilities.assert_condition(0<len(versions),"Version-list can not be empty.")
212
+ latest = max(versions, key=Version)
213
+ return latest
214
+
215
+ @staticmethod
216
+ @check_arguments
217
+ def filter_versions_by_prefix(versions:list[str],prefix:str) -> list[str]:
218
+ return [v for v in versions if v.startswith(prefix)]
219
+
220
+ @staticmethod
221
+ @check_arguments
222
+ def get_major_part_of_version(version:str) -> int:
223
+ return GeneralUtilities.get_version_parts(version)[0]
224
+
225
+ @staticmethod
226
+ @check_arguments
227
+ def get_minor_part_of_version(version:str) -> int:
228
+ return GeneralUtilities.get_version_parts(version)[1]
229
+
230
+ @staticmethod
231
+ @check_arguments
232
+ def get_patch_part_of_version(version:str) -> int:
233
+ return GeneralUtilities.get_version_parts(version)[2]
234
+
235
+ @staticmethod
236
+ @check_arguments
237
+ def get_version_parts(version:str) -> tuple[int,int,int]:
238
+ match = re.match(r"^(\d+).(\d+).(\d+)$", version)
239
+ GeneralUtilities.assert_condition(match is not None,f"string \"{version}\" is not a valid version.")
240
+ return (int(match.group(1)),int(match.group(2)),int(match.group(3)))
241
+
242
+ @staticmethod
243
+ @check_arguments
244
+ def is_ignored_by_glob_pattern(source_directory:str,path:str, ignored_glob_patterms: list[str]) -> bool:
245
+ source_directory=GeneralUtilities.normalize_path(source_directory)
246
+ path=GeneralUtilities.normalize_path(path)
247
+ GeneralUtilities.assert_condition(path.startswith(source_directory), f"Path '{path}' is not located in source directory '{source_directory}'.")
248
+ if ignored_glob_patterms is None:
249
+ return False
250
+ relative_path = os.path.relpath(path, source_directory)
251
+ for pattern in ignored_glob_patterms:
252
+ if fnmatch.filter([relative_path], pattern):
253
+ return True
254
+ return False
255
+
256
+ @staticmethod
257
+ @check_arguments
258
+ def safe_copy(src:str, dst:str):
259
+ shutil.copy2(src, dst)
260
+ src_size = os.path.getsize(src)
261
+ dst_size = os.path.getsize(dst)
262
+ if src_size != dst_size:
263
+ raise IOError(f"Copy failed: size mismatch {src} ({src_size}) != {dst} ({dst_size})")
264
+
265
+ @staticmethod
266
+ @check_arguments
267
+ def __copy_or_move_content_of_folder(source_directory: str, target_directory: str, overwrite_existing_files:bool, remove_source: bool,ignored_glob_patterms: list[str] = None) -> None:
268
+ srcDirFull = GeneralUtilities.resolve_relative_path_from_current_working_directory(source_directory)
269
+ dstDirFull = GeneralUtilities.resolve_relative_path_from_current_working_directory(target_directory)
270
+ if (os.path.isdir(source_directory)):
271
+ GeneralUtilities.ensure_directory_exists(target_directory)
272
+ for file in GeneralUtilities.get_direct_files_of_folder(srcDirFull):
273
+ filename = os.path.basename(file)
274
+ if not GeneralUtilities.is_ignored_by_glob_pattern(source_directory,file, ignored_glob_patterms):
275
+ targetfile = os.path.join(dstDirFull, filename)
276
+ if (os.path.isfile(targetfile)):
277
+ if overwrite_existing_files:
278
+ GeneralUtilities.ensure_file_does_not_exist(targetfile)
279
+ else:
280
+ raise ValueError(f"Targetfile '{targetfile}' does already exist.")
281
+ GeneralUtilities.safe_copy(file, targetfile)
282
+ if remove_source:
283
+ GeneralUtilities.ensure_file_does_not_exist(file)#remove file to save storage
284
+ for sub_folder in GeneralUtilities.get_direct_folders_of_folder(srcDirFull):
285
+ if not GeneralUtilities.is_ignored_by_glob_pattern(source_directory,sub_folder, ignored_glob_patterms):
286
+ foldername = os.path.basename(sub_folder)
287
+ sub_target = os.path.join(dstDirFull, foldername)
288
+ GeneralUtilities.__copy_or_move_content_of_folder(sub_folder, sub_target, overwrite_existing_files, remove_source,ignored_glob_patterms)
289
+ if remove_source:
290
+ GeneralUtilities.ensure_directory_does_not_exist(sub_folder)
291
+ else:
292
+ raise ValueError(f"Folder '{source_directory}' does not exist")
293
+
294
+ @staticmethod
295
+ @check_arguments
296
+ def replace_regex_each_line_of_file(file: str, replace_from_regex: str, replace_to_regex: str, encoding="utf-8", verbose: bool = False) -> None:
297
+ if verbose:
298
+ GeneralUtilities.write_message_to_stdout(f"Replace '{replace_from_regex}' to '{replace_to_regex}' in '{file}'")
299
+ lines=GeneralUtilities.read_lines_from_file(file,encoding)
300
+ replaced_lines = []
301
+ for line in lines:
302
+ replaced_line = re.sub(replace_from_regex, replace_to_regex, line)
303
+ replaced_lines.append(replaced_line)
304
+ GeneralUtilities.write_lines_to_file(file,replaced_lines,encoding)
305
+
306
+ @staticmethod
307
+ @check_arguments
308
+ def replace_regex_in_file(file: str, replace_from_regex: str, replace_to_regex: str, encoding="utf-8") -> None:
309
+ with open(file, encoding=encoding, mode="r") as f:
310
+ content = f.read()
311
+ content = re.sub(replace_from_regex, replace_to_regex, content)
312
+ with open(file, encoding=encoding, mode="w") as f:
313
+ f.write(content)
314
+
315
+ @staticmethod
316
+ @check_arguments
317
+ def replace_xmltag_in_file(file: str, tag: str, new_value: str, encoding="utf-8") -> None:
318
+ GeneralUtilities.assert_condition(tag.isalnum(tag), f"Invalid tag: \"{tag}\"")
319
+ GeneralUtilities.replace_regex_in_file(file, f"<{tag}>.*</{tag}>", f"<{tag}>{new_value}</{tag}>", encoding)
320
+
321
+ @staticmethod
322
+ @check_arguments
323
+ def update_version_in_csproj_file(file: str, target_version: str) -> None:
324
+ GeneralUtilities.replace_xmltag_in_file(file, "Version", target_version)
325
+ GeneralUtilities.replace_xmltag_in_file(file, "AssemblyVersion", target_version + ".0")
326
+ GeneralUtilities.replace_xmltag_in_file(file, "FileVersion", target_version + ".0")
327
+
328
+ @staticmethod
329
+ @check_arguments
330
+ def replace_underscores_in_text(text: str, replacements: dict) -> str:
331
+ changed = True
332
+ while changed:
333
+ changed = False
334
+ for key, value in replacements.items():
335
+ previousValue = text
336
+ text = text.replace(f"__{key}__", value)
337
+ if (not text == previousValue):
338
+ changed = True
339
+ return text
340
+
341
+ @staticmethod
342
+ @check_arguments
343
+ def replace_underscores_in_file(file: str, replacements: dict, encoding: str = "utf-8"):
344
+ text = GeneralUtilities.read_text_from_file(file, encoding)
345
+ text = GeneralUtilities.replace_underscores_in_text(text, replacements)
346
+ GeneralUtilities.write_text_to_file(file, text, encoding)
347
+
348
+ @staticmethod
349
+ @check_arguments
350
+ def print_text(text: str, print_to_stdout: bool = True):
351
+ stream: object = sys.stdout if print_to_stdout else sys.stderr
352
+ GeneralUtilities.__print_text_to_console(text, stream)
353
+
354
+ @staticmethod
355
+ @check_arguments
356
+ def print_text_in_green(text: str, print_to_stdout: bool = True, print_as_color: bool = True):
357
+ GeneralUtilities.print_text_in_color(text, 32, print_to_stdout, print_as_color)
358
+
359
+ @staticmethod
360
+ @check_arguments
361
+ def print_text_in_yellow(text: str, print_to_stdout: bool = True, print_as_color: bool = True):
362
+ GeneralUtilities.print_text_in_color(text, 33, print_to_stdout, print_as_color)
363
+
364
+ @staticmethod
365
+ @check_arguments
366
+ def print_text_in_red(text: str, print_to_stdout: bool = True, print_as_color: bool = True):
367
+ GeneralUtilities.print_text_in_color(text, 31, print_to_stdout, print_as_color)
368
+
369
+ @staticmethod
370
+ @check_arguments
371
+ def print_text_in_cyan(text: str, print_to_stdout: bool = True, print_as_color: bool = True):
372
+ GeneralUtilities.print_text_in_color(text, 36, print_to_stdout, print_as_color)
373
+
374
+ @staticmethod
375
+ @check_arguments
376
+ def print_text_in_color(text: str, colorcode: int, print_to_stdout: bool = True, print_as_color: bool = True):
377
+ stream: object = sys.stdout if print_to_stdout else sys.stderr
378
+ if print_as_color:
379
+ text = f"\033[{colorcode}m{text}\033[0m"
380
+ GeneralUtilities.__print_text_to_console(text, stream)
381
+
382
+ @staticmethod
383
+ @check_arguments
384
+ def __print_text_to_console(text: str, stream: object):
385
+ stream.write(text)
386
+ stream.flush()
387
+
388
+ @staticmethod
389
+ @check_arguments
390
+ def reconfigure_standard_input_and_outputs():
391
+ sys.stdin.reconfigure(encoding='utf-8')
392
+ sys.stderr.reconfigure(encoding='utf-8')
393
+ sys.stdout.reconfigure(encoding='utf-8')
394
+
395
+ @staticmethod
396
+ @check_arguments
397
+ def write_message_to_stdout_advanced(message: str, add_empty_lines: bool = True, adapt_lines: bool = True, append_linebreak: bool = True):
398
+ new_line_character: str = "\n" if append_linebreak else GeneralUtilities.empty_string
399
+ for line in GeneralUtilities.string_to_lines(message, add_empty_lines, adapt_lines):
400
+ sys.stdout.write(GeneralUtilities.str_none_safe(line)+new_line_character)
401
+ sys.stdout.flush()
402
+
403
+ @staticmethod
404
+ @check_arguments
405
+ def write_message_to_stdout(message: str):
406
+ GeneralUtilities.write_message_to_stdout_advanced(message, True, True, True)
407
+
408
+ @staticmethod
409
+ @check_arguments
410
+ def write_message_to_stderr_advanced(message: str, add_empty_lines: bool = True, adapt_lines: bool = True, append_linebreak: bool = True):
411
+ new_line_character: str = "\n" if append_linebreak else GeneralUtilities.empty_string
412
+ for line in GeneralUtilities.string_to_lines(message, add_empty_lines, adapt_lines):
413
+ sys.stderr.write(GeneralUtilities.str_none_safe(line)+new_line_character)
414
+ sys.stderr.flush()
415
+
416
+ @staticmethod
417
+ @check_arguments
418
+ def write_message_to_stderr(message: str):
419
+ GeneralUtilities.write_message_to_stderr_advanced(message, True, True, True)
420
+
421
+ @staticmethod
422
+ @check_arguments
423
+ def get_advanced_errormessage_for_os_error(os_error: OSError) -> str:
424
+ if GeneralUtilities.string_has_content(os_error.filename2):
425
+ secondpath = f" {os_error.filename2}"
426
+ else:
427
+ secondpath = GeneralUtilities.empty_string
428
+ return f"Related path(s): {os_error.filename}{secondpath}"
429
+
430
+ @staticmethod
431
+ @check_arguments
432
+ def write_exception_to_stderr(exception: Exception, extra_message: str = None):
433
+ GeneralUtilities.write_exception_to_stderr_with_traceback(exception, None, extra_message)
434
+
435
+ @staticmethod
436
+ @check_arguments
437
+ def write_exception_to_stderr_with_traceback(exception: Exception, current_traceback=None, extra_message: str = None):
438
+ GeneralUtilities.write_message_to_stderr(GeneralUtilities.exception_to_str(exception,current_traceback,extra_message))
439
+
440
+ @staticmethod
441
+ @check_arguments
442
+ def exception_to_str(exception: Exception, current_traceback=None, extra_message: str = None)->str:
443
+ result=""
444
+ result=result+"Exception("
445
+ result=result+"\n Type: " + str(type(exception))
446
+ result=result+"\n Message: " + str(exception)
447
+ if extra_message is not None:
448
+ result=result+"\n Extra-message: " + str(extra_message)
449
+ if isinstance(exception, OSError):
450
+ result=result+"\n "+GeneralUtilities.get_advanced_errormessage_for_os_error(exception)
451
+ if current_traceback is not None:
452
+ result=result+"\n Traceback:\n" +str(current_traceback.format_exc())
453
+ result=result+"\n)"
454
+ return result
455
+
456
+ @staticmethod
457
+ @check_arguments
458
+ def string_has_content(string: str) -> bool:
459
+ if string is None:
460
+ return False
461
+ else:
462
+ return len(string.strip()) > 0
463
+
464
+ @staticmethod
465
+ @check_arguments
466
+ def datetime_to_string_for_logfile_name(datetime_object: datetime, add_milliseconds: bool ) -> str:
467
+ """returns a string like '2025-08-21T15-30-00+02-00' or '2025-08-21T15-30-00.123+02-00' depending on the value of add_milliseconds"""
468
+ return GeneralUtilities.datetime_to_string_base(datetime_object, add_milliseconds, "T", "-", False)
469
+
470
+ @staticmethod
471
+ @check_arguments
472
+ def datetime_to_string_for_logfile_entry(datetime_object: datetime, add_milliseconds: bool ) -> str:
473
+ """returns a string like '2025-08-21T15:30:00+02:00' or '2025-08-21T15:30:00.123+02:00' depending on the value of add_milliseconds"""
474
+ return GeneralUtilities.datetime_to_string_base(datetime_object, add_milliseconds, "T", ":", False)
475
+
476
+ @staticmethod
477
+ @check_arguments
478
+ def datetime_to_string_for_readable_entry(datetime_object: datetime, add_milliseconds: bool ) -> str:
479
+ """returns a string like '2025-08-21 15-30-00 +02:00' or '2025-08-21 15:30:00.123 +02:00' depending on the value of add_milliseconds"""
480
+ return GeneralUtilities.datetime_to_string_base(datetime_object, add_milliseconds, " ", ":", True)
481
+
482
+ @staticmethod
483
+ @check_arguments
484
+ def datetime_to_string_base(datetime_object: datetime, add_milliseconds: bool ,mid_separator:str,time_separator:str,add_whitespace_before_timezone:bool) -> str:
485
+ dt:datetime=None
486
+ if datetime_object.tzinfo is None:
487
+ dt = datetime_object.replace(tzinfo=timezone.utc) # assume UTC when no timezone is given
488
+ else:
489
+ dt = datetime_object
490
+ pattern = "%Y-%m-%d"+mid_separator+"%H"+time_separator+"%M"+time_separator+"%S"
491
+ if add_milliseconds:
492
+ pattern = pattern+".%f"
493
+ if add_whitespace_before_timezone:
494
+ pattern = pattern+" "
495
+ pattern = pattern+"%z"
496
+
497
+ result = dt.strftime(pattern)
498
+ result = result[:-2] + time_separator + result[-2:]
499
+ return result
500
+
501
+ @staticmethod
502
+ @check_arguments
503
+ def string_has_nonwhitespace_content(string: str) -> bool:
504
+ if string is None:
505
+ return False
506
+ else:
507
+ return len(string.strip()) > 0
508
+
509
+ @staticmethod
510
+ @check_arguments
511
+ def string_is_none_or_empty(argument: str) -> bool:
512
+ if argument is None:
513
+ return True
514
+ type_of_argument = type(argument)
515
+ if type_of_argument == str:
516
+ return argument == GeneralUtilities.empty_string
517
+ else:
518
+ raise ValueError(f"expected string-variable in argument of string_is_none_or_empty but the type was '{str(type_of_argument)}'")
519
+
520
+ @staticmethod
521
+ @check_arguments
522
+ def string_is_none_or_whitespace(string: str) -> bool:
523
+ if GeneralUtilities.string_is_none_or_empty(string):
524
+ return True
525
+ else:
526
+ return string.strip() == GeneralUtilities.empty_string
527
+
528
+ @staticmethod
529
+ @check_arguments
530
+ def strip_new_line_character(value: str) -> str:
531
+ while not GeneralUtilities.__strip_new_line_character_helper_value_is_ok(value):
532
+ value = GeneralUtilities.__strip_new_line_character_helper_normalize_value(value)
533
+ return value
534
+
535
+ @staticmethod
536
+ @check_arguments
537
+ def __strip_new_line_character_helper_value_is_ok(value: str) -> bool:
538
+ if value.startswith("\r") or value.endswith("\r"):
539
+ return False
540
+ if value.startswith("\n") or value.endswith("\n"):
541
+ return False
542
+ return True
543
+
544
+ @staticmethod
545
+ @check_arguments
546
+ def __strip_new_line_character_helper_normalize_value(value: str) -> str:
547
+ return value.strip('\n').strip('\r')
548
+
549
+ @staticmethod
550
+ @check_arguments
551
+ def file_ends_with_newline(file: str) -> bool:
552
+ file=GeneralUtilities.normalize_path(file)
553
+ with open(file, "rb") as file_object:
554
+ return GeneralUtilities.ends_with_newline_character(file_object.read())
555
+
556
+ @staticmethod
557
+ @check_arguments
558
+ def ends_with_newline_character(content: bytes) -> bool:
559
+ result = content.endswith(GeneralUtilities.string_to_bytes("\n"))
560
+ return result
561
+
562
+ @staticmethod
563
+ @check_arguments
564
+ def file_ends_with_content(file: str) -> bool:
565
+ content = GeneralUtilities.read_binary_from_file(file)
566
+ if len(content) == 0:
567
+ return False
568
+ else:
569
+ if GeneralUtilities.ends_with_newline_character(content):
570
+ return False
571
+ else:
572
+ return True
573
+
574
+ @staticmethod
575
+ @check_arguments
576
+ def get_new_line_character_for_textfile_if_required(file: str) -> bool:
577
+ if GeneralUtilities.file_ends_with_content(file):
578
+ return "\n"
579
+ else:
580
+ return GeneralUtilities.empty_string
581
+
582
+ @staticmethod
583
+ @check_arguments
584
+ def append_line_to_file(file: str, line: str, encoding: str = "utf-8") -> None:
585
+ file=GeneralUtilities.normalize_path(file)
586
+ GeneralUtilities.append_lines_to_file(file, [line], encoding)
587
+
588
+ @staticmethod
589
+ @check_arguments
590
+ def append_lines_to_file(file: str, lines: list[str], encoding: str = "utf-8") -> None:
591
+ file=GeneralUtilities.normalize_path(file)
592
+ if len(lines) == 0:
593
+ return
594
+ is_first_line = True
595
+ for line in lines:
596
+ insert_linebreak: bool
597
+ if is_first_line:
598
+ insert_linebreak = GeneralUtilities.file_ends_with_content(file)
599
+ else:
600
+ insert_linebreak = True
601
+ line_to_write: str = None
602
+ if insert_linebreak:
603
+ line_to_write = "\n"+line
604
+ else:
605
+ line_to_write = line
606
+ with open(file, "r+b") as fileObject:
607
+ fileObject.seek(0, os.SEEK_END)
608
+ fileObject.write(GeneralUtilities.string_to_bytes(line_to_write, encoding))
609
+ is_first_line = False
610
+
611
+ @staticmethod
612
+ @check_arguments
613
+ def append_to_file(file: str, content: str, encoding: str = "utf-8") -> None:
614
+ file=GeneralUtilities.normalize_path(file)
615
+ GeneralUtilities.assert_condition(not "\n" in content, "Appending multiple lines is not allowed. Use append_lines_to_file instead.")
616
+ with open(file, "a", encoding=encoding) as fileObject:
617
+ fileObject.write(content)
618
+
619
+ @staticmethod
620
+ @check_arguments
621
+ def ensure_directory_exists(path: str) -> None:
622
+ path=GeneralUtilities.normalize_path(path)
623
+ if not os.path.isdir(path):
624
+ os.makedirs(path)
625
+
626
+ @staticmethod
627
+ @check_arguments
628
+ def ensure_file_exists(path: str) -> None:
629
+ path=GeneralUtilities.normalize_path(path)
630
+ if (not os.path.isfile(path)):
631
+ with open(path, "a+", encoding="utf-8"):
632
+ pass
633
+
634
+ @staticmethod
635
+ @check_arguments
636
+ def __remove_readonly(func, path, _):
637
+ os.chmod(path, stat.S_IWRITE)
638
+ func(path)
639
+
640
+ @staticmethod
641
+ @check_arguments
642
+ def __rmtree(directory: str) -> None:
643
+ shutil.rmtree(directory, onerror=GeneralUtilities.__remove_readonly) # pylint: disable=deprecated-argument
644
+
645
+ @staticmethod
646
+ @check_arguments
647
+ def ensure_directory_does_not_exist(path: str) -> None:
648
+ if (os.path.isdir(path)):
649
+ for root, dirs, files in os.walk(path, topdown=False):
650
+ for name in files:
651
+ filename = os.path.join(root, name)
652
+ os.chmod(filename, stat.S_IWUSR)
653
+ os.remove(filename)
654
+ for name in dirs:
655
+ GeneralUtilities.__rmtree(os.path.join(root, name))
656
+ GeneralUtilities.__rmtree(path)
657
+
658
+ @staticmethod
659
+ @check_arguments
660
+ def ensure_folder_exists_and_is_empty(path: str) -> None:
661
+ GeneralUtilities.ensure_directory_exists(path)
662
+ for filename in os.listdir(path):
663
+ file_path = os.path.join(path, filename)
664
+ if os.path.isfile(file_path):
665
+ os.remove(file_path)
666
+ if os.path.islink(file_path):
667
+ os.unlink(file_path)
668
+ elif os.path.isdir(file_path):
669
+ shutil.rmtree(file_path)
670
+
671
+ @staticmethod
672
+ @check_arguments
673
+ def ensure_file_does_not_exist(path: str) -> None:
674
+ if (os.path.isfile(path)):
675
+ os.remove(path)
676
+
677
+ @staticmethod
678
+ @check_arguments
679
+ def ensure_path_does_not_exist(path: str) -> None:
680
+ if (os.path.isfile(path)):
681
+ GeneralUtilities.ensure_file_does_not_exist(path)
682
+ if (os.path.isdir(path)):
683
+ GeneralUtilities.ensure_directory_does_not_exist(path)
684
+
685
+ @staticmethod
686
+ @check_arguments
687
+ def format_xml_file(filepath: str) -> None:
688
+ GeneralUtilities.format_xml_file_with_encoding(filepath, "utf-8")
689
+
690
+ @staticmethod
691
+ @check_arguments
692
+ def format_xml_file_with_encoding(filepath: str, encoding: str) -> None:
693
+ with open(filepath, 'r', encoding=encoding) as file:
694
+ text = file.read()
695
+ text = parse(text).toprettyxml()
696
+ with open(filepath, 'w', encoding=encoding) as file:
697
+ file.write(text)
698
+
699
+ @staticmethod
700
+ @check_arguments
701
+ def get_clusters_and_sectors_of_disk(diskpath: str) -> None:
702
+ GeneralUtilities.assert_condition(GeneralUtilities.current_system_is_windows(), "get_clusters_and_sectors_of_disk(diskpath) is only available on windows.")
703
+ sectorsPerCluster = ctypes.c_ulonglong(0)
704
+ bytesPerSector = ctypes.c_ulonglong(0)
705
+ rootPathName = ctypes.c_wchar_p(diskpath)
706
+ ctypes.windll.kernel32.GetDiskFreeSpaceW(rootPathName, ctypes.pointer(sectorsPerCluster), ctypes.pointer(bytesPerSector), None, None)
707
+ return (sectorsPerCluster.value, bytesPerSector.value)
708
+
709
+ @staticmethod
710
+ @check_arguments
711
+ def ensure_path_is_not_quoted(path: str) -> str:
712
+ if (path.startswith("\"") and path.endswith("\"")) or (path.startswith("'") and path.endswith("'")):
713
+ path = path[1:]
714
+ path = path[:-1]
715
+ return path
716
+ else:
717
+ return path
718
+
719
+ @staticmethod
720
+ @check_arguments
721
+ def get_missing_files(folderA: str, folderB: str) -> list:
722
+ folderA_length = len(folderA)
723
+ result = []
724
+ for fileA in GeneralUtilities.absolute_file_paths(folderA):
725
+ file = fileA[folderA_length:]
726
+ fileB = folderB + file
727
+ if not os.path.isfile(fileB):
728
+ result.append(fileB)
729
+ return result
730
+
731
+ @staticmethod
732
+ @check_arguments
733
+ def to_pascal_case(s: str) -> str:
734
+ return ''.join(current.lower() if prev.isalnum() else current.upper() for prev, current in zip(' ' + s, s) if current.isalnum())
735
+
736
+ @staticmethod
737
+ @check_arguments
738
+ def to_snake_case(s: str) -> str:
739
+ return '_'.join(t.lower() for t in re.split(r'[^a-zA-Z0-9]+', s) if t)
740
+
741
+ @staticmethod
742
+ @check_arguments
743
+ def to_camel_case(s: str) -> str:
744
+ words = [t.lower() for t in re.split(r'[^a-zA-Z0-9]+', s) if t]
745
+ return words[0] + ''.join(w.capitalize() for w in words[1:]) if words else ''
746
+
747
+ @staticmethod
748
+ @check_arguments
749
+ def to_kebab_case(s: str) -> str:
750
+ return '-'.join(t.lower() for t in re.split(r'[^a-zA-Z0-9]+', s) if t)
751
+
752
+ @staticmethod
753
+ @check_arguments
754
+ def find_between(s: str, start: str, end: str) -> str:
755
+ return s.split(start)[1].split(end)[0]
756
+
757
+ @staticmethod
758
+ @check_arguments
759
+ def write_lines_to_file(file: str, lines: list, encoding="utf-8") -> None:
760
+ lines = [GeneralUtilities.strip_new_line_character(line) for line in lines]
761
+ content = "\n".join(lines)
762
+ GeneralUtilities.write_text_to_file(file, content, encoding)
763
+
764
+ @staticmethod
765
+ @check_arguments
766
+ def write_text_to_file(file: str, content: str, encoding="utf-8") -> None:
767
+ GeneralUtilities.write_binary_to_file(file, bytes(bytearray(content, encoding)))
768
+
769
+ @staticmethod
770
+ @check_arguments
771
+ def write_binary_to_file(file: str, content: bytes) -> None:
772
+ with open(file, "wb") as file_object:
773
+ file_object.write(content)
774
+
775
+ @staticmethod
776
+ def is_binary_file(path: str):
777
+ content = GeneralUtilities.read_binary_from_file(path)
778
+ binary_content_indicators = [b'\x00', b'\x01', b'\x02', b'\x03', b'\x04', b'\x05', b'\x06', b'\x07', b'\x08', b'\x0E', b'\x1F']
779
+ for binary_content_indicator in binary_content_indicators:
780
+ if binary_content_indicator in content:
781
+ return True
782
+ return False
783
+
784
+ @staticmethod
785
+ @check_arguments
786
+ def read_lines_from_file(file: str, encoding="utf-8") -> list[str]:
787
+ content = GeneralUtilities.read_text_from_file(file, encoding)
788
+ if len(content) == 0:
789
+ return []
790
+ else:
791
+ return [GeneralUtilities.strip_new_line_character(line) for line in content.split('\n')]
792
+
793
+ @staticmethod
794
+ @check_arguments
795
+ def read_nonempty_lines_from_file(file: str, encoding="utf-8") -> list[str]:
796
+ return [line for line in GeneralUtilities.read_lines_from_file(file, encoding) if GeneralUtilities.string_has_content(line)]
797
+
798
+ @staticmethod
799
+ @check_arguments
800
+ def read_text_from_file(file: str, encoding="utf-8") -> str:
801
+ GeneralUtilities.assert_file_exists(file)
802
+ return GeneralUtilities.bytes_to_string(GeneralUtilities.read_binary_from_file(file), encoding)
803
+
804
+ @staticmethod
805
+ @check_arguments
806
+ def read_text_from_file_without_linebreak(file: str, encoding="utf-8") -> str:
807
+ return GeneralUtilities.read_text_from_file(file,encoding).replace("\n","").replace("\r","")
808
+
809
+ @staticmethod
810
+ @check_arguments
811
+ def read_binary_from_file(file: str) -> bytes:
812
+ file=GeneralUtilities.normalize_path(file)
813
+ with open(file, "rb") as file_object:
814
+ return file_object.read()
815
+
816
+ @staticmethod
817
+ @check_arguments
818
+ def timedelta_to_simple_string(delta: timedelta) -> str:
819
+ return (datetime(1970, 1, 1, 0, 0, 0) + delta).strftime('%H:%M:%S')
820
+
821
+ @staticmethod
822
+ @check_arguments
823
+ def resolve_relative_path_from_current_working_directory(path: str) -> str:
824
+ return GeneralUtilities.resolve_relative_path(path, os.getcwd())
825
+
826
+ @staticmethod
827
+ @check_arguments
828
+ def resolve_relative_path(path: str, base_path: str):
829
+ if (os.path.isabs(path)):
830
+ return path
831
+ else:
832
+ return str(Path(os.path.join(base_path, path)).resolve())
833
+
834
+ @staticmethod
835
+ @check_arguments
836
+ def get_metadata_for_file_for_clone_folder_structure(file: str) -> str:
837
+ size = os.path.getsize(file)
838
+ last_modified_timestamp = os.path.getmtime(file)
839
+ hash_value = GeneralUtilities.get_sha256_of_file(file)
840
+ last_access_timestamp = os.path.getatime(file)
841
+ return f'{{"size":"{size}","sha256":"{hash_value}","mtime":"{last_modified_timestamp}","atime":"{last_access_timestamp}"}}'
842
+
843
+ @staticmethod
844
+ @check_arguments
845
+ def clone_folder_structure(source: str, target: str, copy_only_metadata: bool):
846
+ source = GeneralUtilities.resolve_relative_path(source, os.getcwd())
847
+ target = GeneralUtilities.resolve_relative_path(target, os.getcwd())
848
+ length_of_source = len(source)
849
+ for source_file in GeneralUtilities.absolute_file_paths(source):
850
+ target_file = target+source_file[length_of_source:]
851
+ GeneralUtilities.ensure_directory_exists(os.path.dirname(target_file))
852
+ if copy_only_metadata:
853
+ with open(target_file, 'w', encoding='utf8') as f:
854
+ f.write(GeneralUtilities.get_metadata_for_file_for_clone_folder_structure(source_file))
855
+ else:
856
+ shutil.copyfile(source_file, target_file)
857
+
858
+ @staticmethod
859
+ @check_arguments
860
+ def current_user_has_elevated_privileges() -> bool:
861
+ try:
862
+ return os.getuid() == 0
863
+ except AttributeError:
864
+ return ctypes.windll.shell32.IsUserAnAdmin() == 1
865
+
866
+ @staticmethod
867
+ @check_arguments
868
+ def ensure_elevated_privileges() -> None:
869
+ if (not GeneralUtilities.current_user_has_elevated_privileges()):
870
+ raise ValueError("Not enough privileges.")
871
+
872
+ @staticmethod
873
+ @check_arguments
874
+ def rename_names_of_all_files_and_folders(folder: str, replace_from: str, replace_to: str, replace_only_full_match=False):
875
+ for file in GeneralUtilities.get_direct_files_of_folder(folder):
876
+ GeneralUtilities.replace_in_filename(file, replace_from, replace_to, replace_only_full_match)
877
+ for sub_folder in GeneralUtilities.get_direct_folders_of_folder(folder):
878
+ GeneralUtilities.rename_names_of_all_files_and_folders(sub_folder, replace_from, replace_to, replace_only_full_match)
879
+ GeneralUtilities.replace_in_foldername(folder, replace_from, replace_to, replace_only_full_match)
880
+
881
+ @staticmethod
882
+ @check_arguments
883
+ def get_direct_files_of_folder(folder: str) -> list[str]:
884
+ result = [os.path.join(folder, f) for f in listdir(folder) if isfile(join(folder, f))]
885
+ result = sorted(result, key=str.casefold)
886
+ return result
887
+
888
+ @staticmethod
889
+ @check_arguments
890
+ def get_direct_folders_of_folder(folder: str) -> list[str]:
891
+ result = [os.path.join(folder, f) for f in listdir(folder) if isdir(join(folder, f))]
892
+ result = sorted(result, key=str.casefold)
893
+ return result
894
+
895
+ @staticmethod
896
+ @check_arguments
897
+ def get_all_files_of_folder(folder: str) -> list[str]:
898
+ result = list()
899
+ result.extend(GeneralUtilities.get_direct_files_of_folder(folder))
900
+ for subfolder in GeneralUtilities.get_direct_folders_of_folder(folder):
901
+ result.extend(GeneralUtilities.get_all_files_of_folder(subfolder))
902
+ result = sorted(result, key=str.casefold)
903
+ return result
904
+
905
+ @staticmethod
906
+ @check_arguments
907
+ def get_all_folders_of_folder(folder: str) -> list[str]:
908
+ result = list()
909
+ subfolders = GeneralUtilities.get_direct_folders_of_folder(folder)
910
+ result.extend(subfolders)
911
+ for subfolder in subfolders:
912
+ result.extend(GeneralUtilities.get_all_folders_of_folder(subfolder))
913
+ result = sorted(result, key=str.casefold)
914
+ return result
915
+
916
+ @staticmethod
917
+ @check_arguments
918
+ def get_all_objects_of_folder(folder: str) -> list[str]:
919
+ return sorted(GeneralUtilities.get_all_files_of_folder(folder) + GeneralUtilities.get_all_folders_of_folder(folder), key=str.casefold)
920
+
921
+ @staticmethod
922
+ @check_arguments
923
+ def replace_in_filename(file: str, replace_from: str, replace_to: str, replace_only_full_match=False):
924
+ filename = Path(file).name
925
+ if (GeneralUtilities.__should_get_replaced(filename, replace_from, replace_only_full_match)):
926
+ folder_of_file = os.path.dirname(file)
927
+ os.rename(file, os.path.join(folder_of_file, filename.replace(replace_from, replace_to)))
928
+
929
+ @staticmethod
930
+ @check_arguments
931
+ def replace_in_foldername(folder: str, replace_from: str, replace_to: str, replace_only_full_match=False):
932
+ foldername = Path(folder).name
933
+ if (GeneralUtilities.__should_get_replaced(foldername, replace_from, replace_only_full_match)):
934
+ folder_of_folder = os.path.dirname(folder)
935
+ os.rename(folder, os.path.join(folder_of_folder, foldername.replace(replace_from, replace_to)))
936
+
937
+ @staticmethod
938
+ @check_arguments
939
+ def __should_get_replaced(input_text, search_text, replace_only_full_match) -> bool:
940
+ if replace_only_full_match:
941
+ return input_text == search_text
942
+ else:
943
+ return search_text in input_text
944
+
945
+ @staticmethod
946
+ @check_arguments
947
+ def str_none_safe(variable) -> str:
948
+ if variable is None:
949
+ return ''
950
+ else:
951
+ return str(variable)
952
+
953
+ @staticmethod
954
+ @check_arguments
955
+ def arguments_to_array(arguments_as_string: str) -> list[str]:
956
+ if arguments_as_string is None:
957
+ return []
958
+ if GeneralUtilities.string_has_content(arguments_as_string):
959
+ return arguments_as_string.split(" ") # TODO this function should get improved to allow whitespaces in quote-substrings
960
+ else:
961
+ return []
962
+
963
+ @staticmethod
964
+ @check_arguments
965
+ def arguments_to_array_for_log(arguments_as_string: str) -> list[str]:
966
+ if arguments_as_string is None:
967
+ return None
968
+ return GeneralUtilities.arguments_to_array(arguments_as_string)
969
+
970
+ @staticmethod
971
+ @check_arguments
972
+ def get_sha256_of_file(file: str) -> str:
973
+ sha256 = hashlib.sha256()
974
+ with open(file, "rb") as fileObject:
975
+ for chunk in iter(lambda: fileObject.read(4096), b""):
976
+ sha256.update(chunk)
977
+ return sha256.hexdigest()
978
+
979
+ @staticmethod
980
+ @check_arguments
981
+ def remove_duplicates(input_list) -> list:
982
+ result = []
983
+ for item in input_list:
984
+ if not item in result:
985
+ result.append(item)
986
+ return result
987
+
988
+ @staticmethod
989
+ @check_arguments
990
+ def print_stacktrace() -> None:
991
+ for line in traceback.format_stack():
992
+ GeneralUtilities.write_message_to_stderr(line.strip())
993
+
994
+ @staticmethod
995
+ @check_arguments
996
+ def string_to_boolean(value: str) -> bool:
997
+ value = value.strip().lower()
998
+ if value in ('yes', 'y', 'true', 't', '1'):
999
+ return True
1000
+ elif value in ('no', 'n', 'false', 'f', '0'):
1001
+ return False
1002
+ else:
1003
+ raise ValueError(f"Can not convert '{value}' to a boolean value")
1004
+
1005
+ @staticmethod
1006
+ @check_arguments
1007
+ def file_is_empty(file: str) -> bool:
1008
+ return os.stat(file).st_size == 0
1009
+
1010
+ @staticmethod
1011
+ @check_arguments
1012
+ def folder_is_empty(folder: str) -> bool:
1013
+ return len(GeneralUtilities.get_direct_files_of_folder(folder)) == 0 and len(GeneralUtilities.get_direct_folders_of_folder(folder)) == 0
1014
+
1015
+ @staticmethod
1016
+ @check_arguments
1017
+ def get_time_based_logfile_by_folder(folder: str, name: str = "Log") -> str:
1018
+ return os.path.join(GeneralUtilities.resolve_relative_path_from_current_working_directory(folder), f"{GeneralUtilities.get_time_based_logfilename(name)}.log")
1019
+
1020
+ @staticmethod
1021
+ @check_arguments
1022
+ def get_now() -> datetime:
1023
+ return datetime.now().astimezone().replace(microsecond=0)
1024
+
1025
+ @staticmethod
1026
+ @check_arguments
1027
+ def get_time_based_logfilename(name: str = "Log") -> str:
1028
+ d = GeneralUtilities.get_now()
1029
+ return f"{name}_{GeneralUtilities.datetime_to_string_for_logfile_name(d,False)}"
1030
+
1031
+ @staticmethod
1032
+ @check_arguments
1033
+ def bytes_to_string(payload: bytes, encoding: str = 'utf-8') -> str:
1034
+ return payload.decode(encoding, errors="ignore")
1035
+
1036
+ @staticmethod
1037
+ @check_arguments
1038
+ def string_to_bytes(payload: str, encoding: str = 'utf-8') -> bytes:
1039
+ return payload.encode(encoding, errors="ignore")
1040
+
1041
+ @staticmethod
1042
+ @check_arguments
1043
+ def contains_line(lines, regex: str) -> bool:
1044
+ for line in lines:
1045
+ if (re.match(regex, line)):
1046
+ return True
1047
+ return False
1048
+
1049
+ @staticmethod
1050
+ @check_arguments
1051
+ def read_csv_file(file: str, ignore_first_line: bool = False, treat_number_sign_at_begin_of_line_as_comment: bool = True, trim_values: bool = True, encoding="utf-8", ignore_empty_lines: bool = True, separator_character: str = ";", values_are_surrounded_by_quotes: bool = False) -> list[list[str]]:
1052
+ lines = GeneralUtilities.read_lines_from_file(file, encoding)
1053
+
1054
+ if ignore_first_line:
1055
+ lines = lines[1:]
1056
+ result = list()
1057
+ line: str
1058
+ for line_loopvariable in lines:
1059
+ use_line = True
1060
+ line = line_loopvariable
1061
+
1062
+ if trim_values:
1063
+ line = line.strip()
1064
+ if ignore_empty_lines:
1065
+ if not GeneralUtilities.string_has_content(line):
1066
+ use_line = False
1067
+
1068
+ if treat_number_sign_at_begin_of_line_as_comment:
1069
+ if line.startswith("#"):
1070
+ use_line = False
1071
+
1072
+ if use_line:
1073
+ if separator_character in line:
1074
+ raw_values_of_line = GeneralUtilities.to_list(line, separator_character)
1075
+ else:
1076
+ raw_values_of_line = [line]
1077
+ if trim_values:
1078
+ raw_values_of_line = [value.strip() for value in raw_values_of_line]
1079
+ values_of_line = []
1080
+ for raw_value_of_line in raw_values_of_line:
1081
+ value_of_line = raw_value_of_line
1082
+ if values_are_surrounded_by_quotes:
1083
+ value_of_line = value_of_line[1:]
1084
+ value_of_line = value_of_line[:-1]
1085
+ value_of_line = value_of_line.replace('""', '"')
1086
+ values_of_line.append(value_of_line)
1087
+ result.extend([values_of_line])
1088
+ return result
1089
+
1090
+ @staticmethod
1091
+ @check_arguments
1092
+ def epew_is_available() -> bool:
1093
+ return GeneralUtilities.tool_is_available("epew")
1094
+
1095
+ @staticmethod
1096
+ @check_arguments
1097
+ def tool_is_available(toolname: str) -> bool:
1098
+ try:
1099
+ return shutil.which(toolname) is not None
1100
+ except:
1101
+ return False
1102
+
1103
+ @staticmethod
1104
+ @check_arguments
1105
+ @deprecated
1106
+ def absolute_file_paths(directory: str) -> list[str]:
1107
+ return GeneralUtilities.get_all_files_of_folder(directory)
1108
+
1109
+ @staticmethod
1110
+ @check_arguments
1111
+ def to_list(list_as_string: str, separator: str = ",") -> list[str]:
1112
+ result = list()
1113
+ if list_as_string is not None:
1114
+ list_as_string = list_as_string.strip()
1115
+ if list_as_string == GeneralUtilities.empty_string:
1116
+ pass
1117
+ elif separator in list_as_string:
1118
+ for item in list_as_string.split(separator):
1119
+ result.append(item.strip())
1120
+ else:
1121
+ result.append(list_as_string)
1122
+ return result
1123
+
1124
+ @staticmethod
1125
+ @check_arguments
1126
+ def get_next_square_number(number: int) -> int:
1127
+ GeneralUtilities.assert_condition(number >= 0, "get_next_square_number is only applicable for nonnegative numbers")
1128
+ if number == 0:
1129
+ return 1
1130
+ root = 0
1131
+ square = 0
1132
+ while square < number:
1133
+ root = root+1
1134
+ square = root*root
1135
+ return root*root
1136
+
1137
+ @staticmethod
1138
+ @check_arguments
1139
+ def generate_password(length: int = 16, alphabet: str = None) -> None:
1140
+ if alphabet is None:
1141
+ alphabet = strin.ascii_letters + strin.digits+"_"
1142
+ return ''.join(secrets.choice(alphabet) for i in range(length))
1143
+
1144
+ @staticmethod
1145
+ @check_arguments
1146
+ def assert_condition(condition: bool, information: str = None) -> None:
1147
+ """Throws an exception if the condition is false."""
1148
+ if (not condition):
1149
+ if information is None:
1150
+ information = "Internal assertion error."
1151
+ raise ValueError("Condition failed. "+information)
1152
+
1153
+ @staticmethod
1154
+ def current_system_is_windows():
1155
+ return platform.system() == 'Windows'
1156
+
1157
+ @staticmethod
1158
+ def current_system_is_linux():
1159
+ return platform.system() == 'Linux'
1160
+
1161
+ @staticmethod
1162
+ @check_arguments
1163
+ def get_line():
1164
+ return "--------------------------"
1165
+
1166
+ @staticmethod
1167
+ def get_longline():
1168
+ return GeneralUtilities.get_line() + GeneralUtilities.get_line()
1169
+
1170
+ @staticmethod
1171
+ @check_arguments
1172
+ def get_icon_check_empty(positive: bool) -> str:
1173
+ if positive:
1174
+ return "✅"
1175
+ else:
1176
+ return GeneralUtilities.empty_string
1177
+
1178
+ @staticmethod
1179
+ @check_arguments
1180
+ def get_icon_check_cross(positive: bool) -> str:
1181
+ if positive:
1182
+ return "✅"
1183
+ else:
1184
+ return "❌"
1185
+
1186
+ @staticmethod
1187
+ @check_arguments
1188
+ def get_certificate_expiry_date(certificate_file: str) -> datetime:
1189
+ with open(certificate_file, encoding="utf-8") as certificate_file_content:
1190
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, certificate_file_content.read())
1191
+ date_as_bytes = cert.get_notAfter()
1192
+ date_as_string = date_as_bytes.decode("utf-8")
1193
+ result = datetime.strptime(date_as_string, '%Y%m%d%H%M%SZ')
1194
+ return result
1195
+
1196
+ @staticmethod
1197
+ @check_arguments
1198
+ def certificate_is_expired(certificate_file: str) -> bool:
1199
+ return GeneralUtilities.get_certificate_expiry_date(certificate_file) < GeneralUtilities.get_now()
1200
+
1201
+ @staticmethod
1202
+ @check_arguments
1203
+ def internet_connection_is_available() -> bool:
1204
+ # TODO add more hosts to check to return true if at least one is available
1205
+ try:
1206
+ with urllib.request.urlopen("https://www.google.com") as url_result:
1207
+ return (url_result.code // 100) == 2
1208
+ except:
1209
+ pass
1210
+ return False
1211
+
1212
+ @staticmethod
1213
+ @check_arguments
1214
+ def replace_variable_in_string(input_string: str, variable_name: str, variable_value: str) -> str:
1215
+ GeneralUtilities.assert_condition(not "__" in variable_name, f"'{variable_name}' is an invalid variable name because it contains '__' which is treated as control-sequence.")
1216
+ return input_string.replace(f"__[{variable_name}]__", variable_value)
1217
+
1218
+ @staticmethod
1219
+ @check_arguments
1220
+ def replace_variable(prefix:str, variable_name:str, suffix:str, value:str, content: str) -> str:
1221
+ GeneralUtilities.assert_condition(not "__" in variable_name, f"'{variable_name}' is an invalid variable name because it contains '__' which is treated as control-sequence.")
1222
+ pattern = re.escape(GeneralUtilities.str_none_safe( prefix)) + r"\s*" +"__"+ re.escape(variable_name) + "__"+r"\s*" + re.escape(GeneralUtilities.str_none_safe( suffix))
1223
+ result:str= re.sub(pattern, value, content)
1224
+ GeneralUtilities.assert_condition(not f"__{variable_name}__" in result, f"Variable '{variable_name}' was not replaced in the content. This is likely caused by an error in the content or the variable name.")
1225
+ return result
1226
+
1227
+ @staticmethod
1228
+ @check_arguments
1229
+ def input(prompt: str, print_result: bool) -> str: # This function is a workaround for usescases like python scripts which calls input(...) using epew because then the prompt is not printed by the built-in-input-function.
1230
+ GeneralUtilities.write_message_to_stdout(prompt)
1231
+ result: str = input()
1232
+ if print_result:
1233
+ GeneralUtilities.write_message_to_stdout(f"Result: {result}")
1234
+ return result
1235
+
1236
+ @staticmethod
1237
+ @check_arguments
1238
+ def run_program_simple(program: str, arguments: list[str], cwd: str = None) -> tuple[int, str, str]:
1239
+ if cwd is None:
1240
+ cwd = os.getcwd()
1241
+ cmd = [program]+arguments
1242
+ with subprocess.Popen(cmd, cwd=cwd, stderr=subprocess.PIPE, stdout=subprocess.PIPE) as process:
1243
+ stdout, stderr = process.communicate()
1244
+ exit_code = process.wait()
1245
+ return (exit_code, stdout, stderr)
1246
+
1247
+ @staticmethod
1248
+ @check_arguments
1249
+ def assert_file_exists(file: str,message=None) -> None:
1250
+ if message is None:
1251
+ message=f"File '{file}' does not exist."
1252
+ GeneralUtilities.assert_condition(os.path.isfile(file), message)
1253
+
1254
+ @staticmethod
1255
+ @check_arguments
1256
+ def assert_file_does_not_exist(file: str,message=None) -> None:
1257
+ if message is None:
1258
+ message=f"File '{file}' exists."
1259
+ GeneralUtilities.assert_condition(not os.path.isfile(file), message)
1260
+
1261
+ @staticmethod
1262
+ @check_arguments
1263
+ def assert_folder_exists(folder: str,message=None) -> None:
1264
+ if message is None:
1265
+ message=f"Folder '{folder}' does not exist."
1266
+ GeneralUtilities.assert_condition(os.path.isdir(folder),message )
1267
+
1268
+ @staticmethod
1269
+ @check_arguments
1270
+ def assert_folder_does_not_exist(folder: str,message=None) -> None:
1271
+ if message is None:
1272
+ message= f"Folder '{folder}' exists."
1273
+ GeneralUtilities.assert_condition(not os.path.isdir(folder), f"Folder '{folder}' exists.")
1274
+
1275
+ @staticmethod
1276
+ @check_arguments
1277
+ def assert_not_null(obj,message:str=None) -> str:
1278
+ if message is None:
1279
+ message="Variable is not set"
1280
+ GeneralUtilities.assert_condition(obj is not None, message)
1281
+
1282
+ @staticmethod
1283
+ @check_arguments
1284
+ def retry_action(action, amount_of_attempts: int, action_name: str = None,delay_in_seconds:int=2) -> None:
1285
+ amount_of_fails = 0
1286
+ last_exception:Exception=None
1287
+ GeneralUtilities.assert_condition(0<amount_of_attempts,"amount_of_attempts must be greater than 0.")
1288
+ while amount_of_fails<amount_of_attempts:
1289
+ try:
1290
+ result = action()
1291
+ return result
1292
+ except Exception as e:
1293
+ time.sleep(delay_in_seconds)
1294
+ amount_of_fails = amount_of_fails+1
1295
+ last_exception=e
1296
+ GeneralUtilities.assert_not_null(last_exception)
1297
+ message = "Action"
1298
+ if action_name is not None:
1299
+ message = f"{message} \"{action_name}\""
1300
+ message = f"{message} failed {amount_of_attempts} time(s)."
1301
+ GeneralUtilities.write_message_to_stderr(message)
1302
+ raise last_exception
1303
+
1304
+
1305
+ @staticmethod
1306
+ @check_arguments
1307
+ def normalize_path(path)->str:
1308
+ path=str(path)
1309
+ if GeneralUtilities.current_system_is_windows():
1310
+ path=path.replace("/","\\")
1311
+ else:
1312
+ path=path.replace("\\","/")
1313
+ return path
1314
+
1315
+ @staticmethod
1316
+ @check_arguments
1317
+ def int_to_string(number: int, leading_zeroplaces: int, trailing_zeroplaces: int) -> str:
1318
+ return GeneralUtilities.float_to_string(float(number), leading_zeroplaces, trailing_zeroplaces)
1319
+
1320
+ @staticmethod
1321
+ @check_arguments
1322
+ def float_to_string(number: float, leading_zeroplaces: int, trailing_zeroplaces: int) -> str:
1323
+ plain_str = str(number)
1324
+ GeneralUtilities.assert_condition("." in plain_str)
1325
+ splitted: list[str] = plain_str.split(".")
1326
+ return splitted[0].zfill(leading_zeroplaces)+"."+splitted[1].ljust(trailing_zeroplaces, '0')
1327
+
1328
+ @staticmethod
1329
+ @check_arguments
1330
+ def process_is_running_by_name(process_name: str) -> bool:
1331
+ processes: list[psutil.Process] = list(psutil.process_iter())
1332
+ for p in processes:
1333
+ if p.name() == process_name:
1334
+ return True
1335
+ return False
1336
+
1337
+ @staticmethod
1338
+ @check_arguments
1339
+ def process_is_running_by_id(process_id: int) -> bool:
1340
+ processes: list[psutil.Process] = list(psutil.process_iter())
1341
+ for p in processes:
1342
+ if p.pid == process_id:
1343
+ return True
1344
+ return False
1345
+
1346
+ @staticmethod
1347
+ @check_arguments
1348
+ def kill_process(process_id: int, include_child_processes: bool) -> bool:
1349
+ if GeneralUtilities. process_is_running_by_id(process_id):
1350
+ GeneralUtilities.write_message_to_stdout(f"Process with id {process_id} is running. Terminating it...")
1351
+ process = psutil.Process(process_id)
1352
+ if include_child_processes:
1353
+ for child in process.children(recursive=True):
1354
+ if GeneralUtilities.process_is_running_by_id(child.pid):
1355
+ child.kill()
1356
+ if GeneralUtilities.process_is_running_by_id(process_id):
1357
+ process.kill()
1358
+ else:
1359
+ GeneralUtilities.write_message_to_stdout(f"Process with id {process_id} is not running anymore.")
1360
+
1361
+ @staticmethod
1362
+ @check_arguments
1363
+ def get_only_item_from_list(list_with_one_element:list):
1364
+ GeneralUtilities.assert_condition(len(list_with_one_element)==1,f"List does not contain exactly one item. It contains {len(list_with_one_element)} items.")
1365
+ return list_with_one_element[0]
1366
+
1367
+ @staticmethod
1368
+ @check_arguments
1369
+ def trim_newlines(s: str) -> str:
1370
+ return s.strip("\n")
1371
+
1372
+ @staticmethod
1373
+ @check_arguments
1374
+ def log_merger(folder:str):
1375
+ """For each log file in this folder this function looks for log-rotation-files and merges them together again into one file."""
1376
+ all_files=GeneralUtilities.get_direct_files_of_folder(folder)
1377
+ for log_file in all_files:
1378
+ if "AutoRename" in log_file:
1379
+ continue
1380
+ filename=os.path.basename(log_file)
1381
+ filename_without_extension=Path(log_file).stem
1382
+ if not ".archive." in filename:
1383
+
1384
+ #merge with rotated logs
1385
+ if filename.endswith(".log") and not ".archive." in filename:
1386
+ rotated_log_files=sorted([f for f in all_files if os.path.basename(f).startswith(filename_without_extension+".archive.")], key=GeneralUtilities._internal_extract_log_file_number)
1387
+ result=GeneralUtilities.empty_string
1388
+ if 0<len(rotated_log_files):
1389
+ for rotated_log_file in rotated_log_files:
1390
+ result+="\n"+GeneralUtilities.read_text_from_file(rotated_log_file)
1391
+ result+="\n"+GeneralUtilities.read_text_from_file(log_file)
1392
+ GeneralUtilities.write_text_to_file(log_file, result)
1393
+ for rotated_log_file in rotated_log_files:
1394
+ GeneralUtilities.ensure_file_does_not_exist(rotated_log_file)
1395
+
1396
+ #normalize
1397
+ logs=GeneralUtilities.read_text_from_file(log_file)
1398
+ logs=logs.replace("\r\n", "\n")
1399
+ logs=logs.replace("\r", GeneralUtilities.empty_string)
1400
+ logs=re.sub(r"\n+", "\n", logs)
1401
+ logs=GeneralUtilities.trim_newlines(logs)
1402
+ GeneralUtilities.write_text_to_file(log_file, logs)
1403
+
1404
+ @staticmethod
1405
+ @check_arguments
1406
+ def _internal_extract_log_file_number(filename: str) -> int:
1407
+ m = re.search(r"\.archive\.(\d+)\.log$", filename)
1408
+ if m:
1409
+ return int(m.group(1))
1410
+ else:
1411
+ raise ValueError(f"Filename '{filename}' does not match the expected pattern for rotated log files.")
1412
+
1413
+ @staticmethod
1414
+ @check_arguments
1415
+ def _internal_load_resource(relative_path: str) -> bytes:
1416
+ res = resources.files("ScriptCollection.Resources")
1417
+ for part in relative_path.split("/"):
1418
+ res = res.joinpath(part)
1419
+ result= res.read_bytes()
1420
+ return result
1421
+
1422
+ @staticmethod
1423
+ @check_arguments
1424
+ def escape_json_string_value(str_value: str) -> str:
1425
+ """For input 'a"b\\c' this function returns 'a\\"b\\\\c'. This function does not add surrounding quotes. It only escapes the characters inside the string."""
1426
+ json_escaped = json.dumps(str_value)
1427
+ return json_escaped[1:-1]
1428
+
1429
+ @staticmethod
1430
+ @check_arguments
1431
+ def escape_json_property_value(str_value: str) -> str:
1432
+ # remove all characters not in [a-zA-Z0-9_]
1433
+ sanitized = re.sub(r'[^a-zA-Z0-9_]', '', str_value)
1434
+
1435
+ # check if result starts with [a-zA-Z_]
1436
+ if not sanitized or len(sanitized) == 0 or not re.match(r'[a-zA-Z_]', sanitized[0]):
1437
+ raise ValueError(f"Invalid JSON property name after sanitizing: '{sanitized}'. Value must start with [a-zA-Z_].")
1438
+
1439
+ return sanitized
1440
+
1441
+ @staticmethod
1442
+ @check_arguments
1443
+ def escape_yaml_string_value(str_value: str) -> str:
1444
+ """For input 'a"b\\c' this function returns 'a\\"b\\\\c'. This function does not add surrounding quotes. It only escapes the characters inside the string."""
1445
+ result= GeneralUtilities.escape_json_string_value(str_value)
1446
+ #do other adaptions here if desired/required
1447
+ return result
1448
+
1449
+ @staticmethod
1450
+ @check_arguments
1451
+ def escape_yaml_property_value(str_value: str) -> str:
1452
+ result= GeneralUtilities.escape_json_property_value(str_value)
1453
+ #do other adaptions here if desired/required
1454
+ return result
1455
+
1456
+
1457
+ @staticmethod
1458
+ @check_arguments
1459
+ def get_current_platform() -> Platform:
1460
+ system = platform.system().lower()
1461
+ machine = platform.machine().lower()
1462
+
1463
+ if system == "windows" and machine in ("x86_64", "amd64"):
1464
+ return Platform.Windows_AMD64
1465
+ elif system == "linux" and machine in ("x86_64", "amd64"):
1466
+ return Platform.Linux_AMD64
1467
+ elif system == "linux" and machine in ("arm64", "aarch64"):
1468
+ return Platform.Linux_ARM64
1469
+ elif system == "darwin" and machine in ("x86_64", "amd64"):
1470
+ return Platform.MacOS_ARM64
1471
+ else:
1472
+ raise ValueError(f"Unsupported platform: {system}/{machine}")
1473
+
1474
+ @staticmethod
1475
+ @check_arguments
1476
+ def current_system_is_x64():
1477
+ arch = platform.machine().lower()
1478
+ return arch in ("x86_64", "amd64")
1479
+
1480
+ @staticmethod
1481
+ @check_arguments
1482
+ def current_system_is_arm64():
1483
+ arch = platform.machine().lower()
1484
+ return arch in ("arm", "aarch64")
1485
+
1486
+ @staticmethod
1487
+ @check_arguments
1488
+ def platform_to_short_str(platform_value: Platform) -> str:
1489
+ mapping = {
1490
+ Platform.Windows_AMD64: "win-x64",
1491
+ Platform.Linux_AMD64: "linux-x64",
1492
+ Platform.Linux_ARM64: "linux-arm64",
1493
+ Platform.MacOS_ARM64: "osx-arm64",
1494
+ }
1495
+ return mapping[platform_value]
1496
+
1497
+ @staticmethod
1498
+ @check_arguments
1499
+ def platform_from_short_str(platform_str: str) -> Platform:
1500
+ mapping = {
1501
+ "win-x64": Platform.Windows_AMD64,
1502
+ "linux-x64": Platform.Linux_AMD64,
1503
+ "linux-arm64": Platform.Linux_ARM64,
1504
+ "osx-arm64": Platform.MacOS_ARM64,
1505
+ }
1506
+ if platform_str not in mapping:
1507
+ raise ValueError(f"Unsupported platform string: {platform_str}")
1508
+ return mapping[platform_str]
1509
+
1510
+ @staticmethod
1511
+ @check_arguments
1512
+ def platform_to_dash_str(platform_value: Platform) -> str:
1513
+ mapping = {
1514
+ Platform.Windows_AMD64: "Windows-x64",
1515
+ Platform.Linux_AMD64: "Linux-x64",
1516
+ Platform.Linux_ARM64: "Linux-arm64",
1517
+ Platform.MacOS_ARM64: "MacOS-arm64",
1518
+ }
1519
+ return mapping[platform_value]
1520
+
1521
+ @staticmethod
1522
+ @check_arguments
1523
+ def platform_from_dash_str(platform_str: str) -> Platform:
1524
+ mapping = {
1525
+ "Windows-x64": Platform.Windows_AMD64,
1526
+ "Linux-x64": Platform.Linux_AMD64,
1527
+ "Linux-arm64": Platform.Linux_ARM64,
1528
+ "MacOS-arm64": Platform.MacOS_ARM64,
1529
+ }
1530
+ if platform_str not in mapping:
1531
+ raise ValueError(f"Unsupported platform string: {platform_str}")
1532
+ return mapping[platform_str]
1533
+
1534
+ @staticmethod
1535
+ @check_arguments
1536
+ def platform_to_docker_platform_str(platform_value: Platform) -> str:
1537
+ mapping = {
1538
+ Platform.Windows_AMD64: "windows/amd64",
1539
+ Platform.Linux_AMD64: "linux/amd64",
1540
+ Platform.Linux_ARM64: "linux/arm64",
1541
+ Platform.MacOS_ARM64: "linux/arm64", # macOS → linux container
1542
+ }
1543
+ return mapping[platform_value]
1544
+
1545
+ @staticmethod
1546
+ @check_arguments
1547
+ def platform_to_dotnet_runtime_identifier(platform_value: Platform) -> str:
1548
+ mapping = {
1549
+ Platform.Windows_AMD64: "win-x64",
1550
+ Platform.Linux_AMD64: "linux-x64",
1551
+ Platform.Linux_ARM64: "linux-arm64",
1552
+ Platform.MacOS_ARM64: "osx-arm64",
1553
+ }
1554
+ return mapping[platform_value]
1555
+
1556
+ @staticmethod
1557
+ @check_arguments
1558
+ def platform_to_go_runtime_identifier(platform_value: Platform) -> str:
1559
+ mapping = {
1560
+ Platform.Linux_AMD64: "amd64",
1561
+ Platform.Linux_ARM64: "arm64",
1562
+ }
1563
+ return mapping[platform_value]
1564
+
1565
+ @staticmethod
1566
+ @check_arguments
1567
+ def get_all_platforms() -> list[Platform]:
1568
+ return [
1569
+ Platform.Windows_AMD64,
1570
+ Platform.Linux_AMD64,
1571
+ Platform.Linux_ARM64,
1572
+ Platform.MacOS_ARM64,
1573
+ ]
1574
+
1575
+ @staticmethod
1576
+ @check_arguments
1577
+ def get_python_executable() -> str:
1578
+ #virtual_env = os.environ.get("VIRTUAL_ENV")
1579
+ #if GeneralUtilities.string_has_content(virtual_env):
1580
+ # if platform.system() == "Windows":
1581
+ # result = os.path.join(virtual_env, "Scripts", "python.exe")
1582
+ # else:
1583
+ # result = os.path.join(virtual_env, "bin", "python")
1584
+ if GeneralUtilities.string_has_content(sys.executable):
1585
+ result = sys.executable
1586
+ else:
1587
+ result = "python"
1588
+ GeneralUtilities.assert_condition(os.path.isfile(result), f"Python executable does not exist: '{result}'")
1589
+ return result