ScriptCollection 3.5.16__py3-none-any.whl → 4.0.78__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 (45) hide show
  1. ScriptCollection/AnionBuildPlatform.py +206 -0
  2. ScriptCollection/{UpdateCertificates.py → CertificateUpdater.py} +69 -46
  3. ScriptCollection/Executables.py +515 -18
  4. ScriptCollection/GeneralUtilities.py +1272 -873
  5. ScriptCollection/ImageUpdater.py +648 -0
  6. ScriptCollection/ProgramRunnerBase.py +10 -10
  7. ScriptCollection/ProgramRunnerMock.py +2 -0
  8. ScriptCollection/ProgramRunnerPopen.py +7 -1
  9. ScriptCollection/ProgramRunnerSudo.py +108 -0
  10. ScriptCollection/SCLog.py +115 -0
  11. ScriptCollection/ScriptCollectionCore.py +942 -266
  12. ScriptCollection/TFCPS/Docker/TFCPS_CodeUnitSpecific_Docker.py +95 -0
  13. ScriptCollection/TFCPS/Docker/__init__.py +0 -0
  14. ScriptCollection/TFCPS/DotNet/CertificateGeneratorInformationBase.py +8 -0
  15. ScriptCollection/TFCPS/DotNet/CertificateGeneratorInformationGenerate.py +6 -0
  16. ScriptCollection/TFCPS/DotNet/CertificateGeneratorInformationNoGenerate.py +7 -0
  17. ScriptCollection/TFCPS/DotNet/TFCPS_CodeUnitSpecific_DotNet.py +485 -0
  18. ScriptCollection/TFCPS/DotNet/__init__.py +0 -0
  19. ScriptCollection/TFCPS/Flutter/TFCPS_CodeUnitSpecific_Flutter.py +130 -0
  20. ScriptCollection/TFCPS/Flutter/__init__.py +0 -0
  21. ScriptCollection/TFCPS/Go/TFCPS_CodeUnitSpecific_Go.py +74 -0
  22. ScriptCollection/TFCPS/Go/__init__.py +0 -0
  23. ScriptCollection/TFCPS/NodeJS/TFCPS_CodeUnitSpecific_NodeJS.py +131 -0
  24. ScriptCollection/TFCPS/NodeJS/__init__.py +0 -0
  25. ScriptCollection/TFCPS/Python/TFCPS_CodeUnitSpecific_Python.py +227 -0
  26. ScriptCollection/TFCPS/Python/__init__.py +0 -0
  27. ScriptCollection/TFCPS/TFCPS_CodeUnitSpecific_Base.py +418 -0
  28. ScriptCollection/TFCPS/TFCPS_CodeUnit_BuildCodeUnit.py +128 -0
  29. ScriptCollection/TFCPS/TFCPS_CodeUnit_BuildCodeUnits.py +136 -0
  30. ScriptCollection/TFCPS/TFCPS_CreateRelease.py +95 -0
  31. ScriptCollection/TFCPS/TFCPS_Generic.py +43 -0
  32. ScriptCollection/TFCPS/TFCPS_MergeToMain.py +122 -0
  33. ScriptCollection/TFCPS/TFCPS_MergeToStable.py +350 -0
  34. ScriptCollection/TFCPS/TFCPS_PreBuildCodeunitsScript.py +47 -0
  35. ScriptCollection/TFCPS/TFCPS_Tools_General.py +1356 -0
  36. ScriptCollection/TFCPS/__init__.py +0 -0
  37. {ScriptCollection-3.5.16.dist-info → scriptcollection-4.0.78.dist-info}/METADATA +23 -22
  38. scriptcollection-4.0.78.dist-info/RECORD +43 -0
  39. {ScriptCollection-3.5.16.dist-info → scriptcollection-4.0.78.dist-info}/WHEEL +1 -1
  40. {ScriptCollection-3.5.16.dist-info → scriptcollection-4.0.78.dist-info}/entry_points.txt +32 -0
  41. ScriptCollection/ProgramRunnerEpew.py +0 -122
  42. ScriptCollection/RPStream.py +0 -42
  43. ScriptCollection/TasksForCommonProjectStructure.py +0 -2625
  44. ScriptCollection-3.5.16.dist-info/RECORD +0 -16
  45. {ScriptCollection-3.5.16.dist-info → scriptcollection-4.0.78.dist-info}/top_level.txt +0 -0
@@ -1,873 +1,1272 @@
1
- import codecs
2
- import platform
3
- import inspect
4
- import ctypes
5
- import hashlib
6
- import re
7
- import os
8
- import shutil
9
- import urllib
10
- import stat
11
- import secrets
12
- import string as strin
13
- import sys
14
- import traceback
15
- import warnings
16
- import functools
17
- from datetime import datetime, timedelta, date
18
- from os import listdir
19
- from os.path import isfile, join, isdir
20
- from pathlib import Path
21
- from shutil import copyfile
22
- import typing
23
- from defusedxml.minidom import parse
24
- from OpenSSL import crypto
25
-
26
-
27
- class GeneralUtilities:
28
-
29
- __datetime_format: str = "%Y-%m-%dT%H:%M:%S"
30
- __date_format: str = "%Y-%m-%d"
31
-
32
- @staticmethod
33
- def get_modest_dark_url() -> str:
34
- return "https://aniondev.github.io/CDN/ScriptCollectionDesigns/ModestDark/Style.css"
35
-
36
- @staticmethod
37
- def is_generic(t: typing.Type):
38
- return hasattr(t, "__origin__")
39
-
40
- @staticmethod
41
- def check_arguments(function):
42
- def __check_function(*args, **named_args):
43
- parameters: list = inspect.getfullargspec(function)[0].copy()
44
- arguments: list = list(tuple(args)).copy()
45
- if "self" in parameters:
46
- parameters.remove("self")
47
- arguments.pop(0)
48
- for index, argument in enumerate(arguments):
49
- if argument is not None: # Check type of None is not possible. None is always a valid argument-value
50
- if parameters[index] in function.__annotations__: # Check if a type-hint for parameter exist. If not, no parameter-type available for argument-type-check
51
- # Check type of arguments if the type is a generic type seems to be impossible.
52
- if not GeneralUtilities.is_generic(function.__annotations__[parameters[index]]):
53
- if not isinstance(argument, function.__annotations__[parameters[index]]):
54
- 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)))
55
- for index, named_argument in enumerate(named_args):
56
- if named_args[named_argument] is not None:
57
- if parameters[index] in function.__annotations__:
58
- if not GeneralUtilities.is_generic(function.__annotations__.get(named_argument)):
59
- if not isinstance(named_args[named_argument], function.__annotations__.get(named_argument)):
60
- 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)}")
61
- return function(*args, **named_args)
62
- __check_function.__doc__ = function.__doc__
63
- return __check_function
64
-
65
- @staticmethod
66
- def deprecated(func):
67
- @functools.wraps(func)
68
- def new_func(*args, **kwargs):
69
- warnings.simplefilter('always', DeprecationWarning)
70
- warnings.warn(f"Call to deprecated function {func.__name__}", category=DeprecationWarning, stacklevel=2)
71
- warnings.simplefilter('default', DeprecationWarning)
72
- return func(*args, **kwargs)
73
- return new_func
74
-
75
- @staticmethod
76
- @check_arguments
77
- def args_array_surround_with_quotes_if_required(arguments: list[str]) -> list[str]:
78
- result = []
79
- for argument in arguments:
80
- if " " in argument and not (argument.startswith('"') and argument.endswith('"')):
81
- result.append(f'"{argument}"')
82
- else:
83
- result.append(argument)
84
- return result
85
-
86
- @staticmethod
87
- @check_arguments
88
- def string_to_lines(string: str, add_empty_lines: bool = True, adapt_lines: bool = True) -> list[str]:
89
- result = list()
90
- if (string is not None):
91
- lines = list()
92
- if ("\n" in string):
93
- lines = string.split("\n")
94
- else:
95
- lines.append(string)
96
- for rawline in lines:
97
- if adapt_lines:
98
- line = rawline.replace("\r", "").strip()
99
- else:
100
- line = rawline
101
- if GeneralUtilities.string_is_none_or_whitespace(line):
102
- if add_empty_lines:
103
- result.append(line)
104
- else:
105
- result.append(line)
106
- return result
107
-
108
- @staticmethod
109
- @check_arguments
110
- def string_to_datetime(value: str) -> datetime:
111
- return datetime.strptime(value, GeneralUtilities.__datetime_format) # value ="2022-10-06T19:26:01" for example
112
-
113
- @staticmethod
114
- @check_arguments
115
- def datetime_to_string(value: datetime) -> str:
116
- return value.strftime(GeneralUtilities.__datetime_format) # returns "2022-10-06T19:26:01" for example
117
-
118
- @staticmethod
119
- @check_arguments
120
- def string_to_date(value: str) -> date:
121
- splitted = value.split("-")
122
- return date(int(splitted[0]), int(splitted[1]), int(splitted[2])) # value ="2022-10-06" for example
123
-
124
- @staticmethod
125
- @check_arguments
126
- def date_to_string(value: date) -> str:
127
- return value.strftime(GeneralUtilities.__date_format) # returns "2022-10-06" for example
128
-
129
- @staticmethod
130
- @check_arguments
131
- def copy_content_of_folder(source_directory: str, target_directory: str, overwrite_existing_files=False) -> None:
132
- GeneralUtilities.__copy_or_move_content_of_folder(source_directory, target_directory, overwrite_existing_files, False)
133
-
134
- @staticmethod
135
- @check_arguments
136
- def move_content_of_folder(source_directory: str, target_directory: str, overwrite_existing_files=False) -> None:
137
- GeneralUtilities.__copy_or_move_content_of_folder(source_directory, target_directory, overwrite_existing_files, True)
138
-
139
- @staticmethod
140
- @check_arguments
141
- def __copy_or_move_content_of_folder(source_directory: str, target_directory: str, overwrite_existing_files, remove_source: bool) -> None:
142
- srcDirFull = GeneralUtilities.resolve_relative_path_from_current_working_directory(source_directory)
143
- dstDirFull = GeneralUtilities.resolve_relative_path_from_current_working_directory(target_directory)
144
- if (os.path.isdir(source_directory)):
145
- GeneralUtilities.ensure_directory_exists(target_directory)
146
- for file in GeneralUtilities.get_direct_files_of_folder(srcDirFull):
147
- filename = os.path.basename(file)
148
- targetfile = os.path.join(dstDirFull, filename)
149
- if (os.path.isfile(targetfile)):
150
- if overwrite_existing_files:
151
- GeneralUtilities.ensure_file_does_not_exist(targetfile)
152
- else:
153
- raise ValueError(f"Targetfile {targetfile} does already exist")
154
- if remove_source:
155
- shutil.move(file, dstDirFull)
156
- else:
157
- shutil.copy(file, dstDirFull)
158
- for sub_folder in GeneralUtilities.get_direct_folders_of_folder(srcDirFull):
159
- foldername = os.path.basename(sub_folder)
160
- sub_target = os.path.join(dstDirFull, foldername)
161
- GeneralUtilities.__copy_or_move_content_of_folder(sub_folder, sub_target, overwrite_existing_files, remove_source)
162
- if remove_source:
163
- GeneralUtilities.ensure_directory_does_not_exist(sub_folder)
164
- else:
165
- raise ValueError(f"Folder '{source_directory}' does not exist")
166
-
167
- @staticmethod
168
- @check_arguments
169
- def replace_regex_each_line_of_file(file: str, replace_from_regex: str, replace_to_regex: str, encoding="utf-8", verbose: bool = False) -> None:
170
- """This function iterates over each line in the file and replaces it by the line which applied regex.
171
- Note: The lines will be taken from open(...).readlines(). So the lines may contain '\\n' or '\\r\\n' for example."""
172
- if verbose:
173
- GeneralUtilities.write_message_to_stdout(f"Replace '{replace_from_regex}' to '{replace_to_regex}' in '{file}'")
174
- with open(file, encoding=encoding, mode="r") as f:
175
- lines = f.readlines()
176
- replaced_lines = []
177
- for line in lines:
178
- replaced_line = re.sub(replace_from_regex, replace_to_regex, line)
179
- replaced_lines.append(replaced_line)
180
- with open(file, encoding=encoding, mode="w") as f:
181
- f.writelines(replaced_lines)
182
-
183
- @staticmethod
184
- @check_arguments
185
- def replace_regex_in_file(file: str, replace_from_regex: str, replace_to_regex: str, encoding="utf-8") -> None:
186
- with open(file, encoding=encoding, mode="r") as f:
187
- content = f.read()
188
- content = re.sub(replace_from_regex, replace_to_regex, content)
189
- with open(file, encoding=encoding, mode="w") as f:
190
- f.write(content)
191
-
192
- @staticmethod
193
- @check_arguments
194
- def replace_xmltag_in_file(file: str, tag: str, new_value: str, encoding="utf-8") -> None:
195
- GeneralUtilities.replace_regex_in_file(file, f"<{tag}>.*</{tag}>", f"<{tag}>{new_value}</{tag}>", encoding)
196
-
197
- @staticmethod
198
- @check_arguments
199
- def update_version_in_csproj_file(file: str, target_version: str) -> None:
200
- GeneralUtilities.replace_xmltag_in_file(file, "Version", target_version)
201
- GeneralUtilities.replace_xmltag_in_file(file, "AssemblyVersion", target_version + ".0")
202
- GeneralUtilities.replace_xmltag_in_file(file, "FileVersion", target_version + ".0")
203
-
204
- @staticmethod
205
- @check_arguments
206
- def replace_underscores_in_text(text: str, replacements: dict) -> str:
207
- changed = True
208
- while changed:
209
- changed = False
210
- for key, value in replacements.items():
211
- previousValue = text
212
- text = text.replace(f"__{key}__", value)
213
- if (not text == previousValue):
214
- changed = True
215
- return text
216
-
217
- @staticmethod
218
- @check_arguments
219
- def replace_underscores_in_file(file: str, replacements: dict, encoding: str = "utf-8"):
220
- text = GeneralUtilities.read_text_from_file(file, encoding)
221
- text = GeneralUtilities.replace_underscores_in_text(text, replacements)
222
- GeneralUtilities.write_text_to_file(file, text, encoding)
223
-
224
- @staticmethod
225
- @check_arguments
226
- def reconfigure_standrd_input_and_outputs():
227
- sys.stdin.reconfigure(encoding='utf-8')
228
- sys.stderr.reconfigure(encoding='utf-8')
229
- sys.stdout.reconfigure(encoding='utf-8')
230
-
231
- @staticmethod
232
- @check_arguments
233
- def write_message_to_stdout(message: str):
234
- for line in GeneralUtilities.string_to_lines(message):
235
- sys.stdout.write(GeneralUtilities.str_none_safe(line)+"\n")
236
- sys.stdout.flush()
237
-
238
- @staticmethod
239
- @check_arguments
240
- def write_message_to_stderr(message: str):
241
- sys.stderr.write(GeneralUtilities.str_none_safe(message)+"\n")
242
- sys.stderr.flush()
243
-
244
- @staticmethod
245
- @check_arguments
246
- def get_advanced_errormessage_for_os_error(os_error: OSError) -> str:
247
- if GeneralUtilities.string_has_content(os_error.filename2):
248
- secondpath = f" {os_error.filename2}"
249
- else:
250
- secondpath = ""
251
- return f"Related path(s): {os_error.filename}{secondpath}"
252
-
253
- @staticmethod
254
- @check_arguments
255
- def write_exception_to_stderr(exception: Exception, extra_message: str = None):
256
- GeneralUtilities.write_exception_to_stderr_with_traceback(exception, None, extra_message)
257
-
258
- @staticmethod
259
- @check_arguments
260
- def write_exception_to_stderr_with_traceback(exception: Exception, current_traceback=None, extra_message: str = None):
261
- GeneralUtilities.write_message_to_stderr("Exception(")
262
- GeneralUtilities.write_message_to_stderr("Type: " + str(type(exception)))
263
- GeneralUtilities.write_message_to_stderr("Message: " + str(exception))
264
- if extra_message is not None:
265
- GeneralUtilities.write_message_to_stderr("Extra-message: " + str(extra_message))
266
- if isinstance(exception, OSError):
267
- GeneralUtilities.write_message_to_stderr(GeneralUtilities.get_advanced_errormessage_for_os_error(exception))
268
- if current_traceback is not None:
269
- GeneralUtilities.write_message_to_stderr("Traceback: " + current_traceback.format_exc())
270
- GeneralUtilities.write_message_to_stderr(")")
271
-
272
- @staticmethod
273
- @check_arguments
274
- def string_has_content(string: str) -> bool:
275
- if string is None:
276
- return False
277
- else:
278
- return len(string) > 0
279
-
280
- @staticmethod
281
- @check_arguments
282
- def datetime_to_string_for_logfile_name(datetime_object: datetime) -> str:
283
- return datetime_object.strftime('%Y-%m-%d_%H-%M-%S')
284
-
285
- @staticmethod
286
- @check_arguments
287
- def datetime_to_string_for_logfile_entry(datetime_object: datetime) -> str:
288
- return datetime_object.strftime('%Y-%m-%d %H:%M:%S')
289
-
290
- @staticmethod
291
- @check_arguments
292
- def string_has_nonwhitespace_content(string: str) -> bool:
293
- if string is None:
294
- return False
295
- else:
296
- return len(string.strip()) > 0
297
-
298
- @staticmethod
299
- @check_arguments
300
- def string_is_none_or_empty(argument: str) -> bool:
301
- if argument is None:
302
- return True
303
- type_of_argument = type(argument)
304
- if type_of_argument == str:
305
- return argument == ""
306
- else:
307
- raise ValueError(f"expected string-variable in argument of string_is_none_or_empty but the type was '{str(type_of_argument)}'")
308
-
309
- @staticmethod
310
- @check_arguments
311
- def string_is_none_or_whitespace(string: str) -> bool:
312
- if GeneralUtilities.string_is_none_or_empty(string):
313
- return True
314
- else:
315
- return string.strip() == ""
316
-
317
- @staticmethod
318
- @check_arguments
319
- def strip_new_line_character(value: str) -> str:
320
- while not GeneralUtilities.__strip_new_line_character_helper_value_is_ok(value):
321
- value = GeneralUtilities.__strip_new_line_character_helper_normalize_value(value)
322
- return value
323
-
324
- @staticmethod
325
- @check_arguments
326
- def __strip_new_line_character_helper_value_is_ok(value: str) -> bool:
327
- if value.startswith("\r") or value.endswith("\r"):
328
- return False
329
- if value.startswith("\n") or value.endswith("\n"):
330
- return False
331
- return True
332
-
333
- @staticmethod
334
- @check_arguments
335
- def __strip_new_line_character_helper_normalize_value(value: str) -> str:
336
- return value.strip('\n').strip('\r')
337
-
338
- @staticmethod
339
- @check_arguments
340
- def file_ends_with_newline(file: str) -> bool:
341
- with open(file, "rb") as file_object:
342
- return GeneralUtilities.ends_with_newline_character(file_object.read())
343
-
344
- @staticmethod
345
- @check_arguments
346
- def ends_with_newline_character(content: bytes) -> bool:
347
- return content.endswith(b'\x0a')
348
-
349
- @staticmethod
350
- @check_arguments
351
- def __get_new_line_character_if_required(file: str) -> bool:
352
- content = GeneralUtilities.read_binary_from_file(file)
353
- if len(content) == 0:
354
- return ""
355
- else:
356
- if GeneralUtilities.ends_with_newline_character(content):
357
- return ""
358
- else:
359
- return "\n"
360
-
361
- @staticmethod
362
- @check_arguments
363
- def append_line_to_file(file: str, line: str, encoding: str = "utf-8") -> None:
364
- line = GeneralUtilities.__get_new_line_character_if_required(file)+line
365
- GeneralUtilities.append_to_file(file, line, encoding)
366
-
367
- @staticmethod
368
- @check_arguments
369
- def append_to_file(file: str, content: str, encoding: str = "utf-8") -> None:
370
- with open(file, "a", encoding=encoding) as fileObject:
371
- fileObject.write(content)
372
-
373
- @staticmethod
374
- @check_arguments
375
- def ensure_directory_exists(path: str) -> None:
376
- if not os.path.isdir(path):
377
- os.makedirs(path)
378
-
379
- @staticmethod
380
- @check_arguments
381
- def ensure_file_exists(path: str) -> None:
382
- if (not os.path.isfile(path)):
383
- with open(path, "a+", encoding="utf-8"):
384
- pass
385
-
386
- @staticmethod
387
- @check_arguments
388
- def __remove_readonly(func, path, _):
389
- os.chmod(path, stat.S_IWRITE)
390
- func(path)
391
-
392
- @staticmethod
393
- @check_arguments
394
- def rmtree(directory: str) -> None:
395
- shutil.rmtree(directory, onerror=GeneralUtilities.__remove_readonly)
396
-
397
- @staticmethod
398
- @check_arguments
399
- def ensure_directory_does_not_exist(path: str) -> None:
400
- if (os.path.isdir(path)):
401
- for root, dirs, files in os.walk(path, topdown=False):
402
- for name in files:
403
- filename = os.path.join(root, name)
404
- os.chmod(filename, stat.S_IWUSR)
405
- os.remove(filename)
406
- for name in dirs:
407
- GeneralUtilities.rmtree(os.path.join(root, name))
408
- GeneralUtilities.rmtree(path)
409
-
410
- @staticmethod
411
- @check_arguments
412
- def ensure_folder_exists_and_is_empty(path: str) -> None:
413
- GeneralUtilities.ensure_directory_exists(path)
414
- for filename in os.listdir(path):
415
- file_path = os.path.join(path, filename)
416
- if os.path.isfile(file_path):
417
- os.remove(file_path)
418
- if os.path.islink(file_path):
419
- os.unlink(file_path)
420
- elif os.path.isdir(file_path):
421
- shutil.rmtree(file_path)
422
-
423
- @staticmethod
424
- @check_arguments
425
- def ensure_file_does_not_exist(path: str) -> None:
426
- if (os.path.isfile(path)):
427
- os.remove(path)
428
-
429
- @staticmethod
430
- @check_arguments
431
- def format_xml_file(filepath: str) -> None:
432
- GeneralUtilities.format_xml_file_with_encoding(filepath, "utf-8")
433
-
434
- @staticmethod
435
- @check_arguments
436
- def format_xml_file_with_encoding(filepath: str, encoding: str) -> None:
437
- with codecs.open(filepath, 'r', encoding=encoding) as file:
438
- text = file.read()
439
- text = parse(text).toprettyxml()
440
- with codecs.open(filepath, 'w', encoding=encoding) as file:
441
- file.write(text)
442
-
443
- @staticmethod
444
- @check_arguments
445
- def get_clusters_and_sectors_of_disk(diskpath: str) -> None:
446
- sectorsPerCluster = ctypes.c_ulonglong(0)
447
- bytesPerSector = ctypes.c_ulonglong(0)
448
- rootPathName = ctypes.c_wchar_p(diskpath)
449
- ctypes.windll.kernel32.GetDiskFreeSpaceW(rootPathName, ctypes.pointer(sectorsPerCluster), ctypes.pointer(bytesPerSector), None, None)
450
- return (sectorsPerCluster.value, bytesPerSector.value)
451
-
452
- @staticmethod
453
- @check_arguments
454
- def ensure_path_is_not_quoted(path: str) -> str:
455
- if (path.startswith("\"") and path.endswith("\"")) or (path.startswith("'") and path.endswith("'")):
456
- path = path[1:]
457
- path = path[:-1]
458
- return path
459
- else:
460
- return path
461
-
462
- @staticmethod
463
- @check_arguments
464
- def get_missing_files(folderA: str, folderB: str) -> list:
465
- folderA_length = len(folderA)
466
- result = []
467
- for fileA in GeneralUtilities.absolute_file_paths(folderA):
468
- file = fileA[folderA_length:]
469
- fileB = folderB + file
470
- if not os.path.isfile(fileB):
471
- result.append(fileB)
472
- return result
473
-
474
- @staticmethod
475
- @check_arguments
476
- def write_lines_to_file(file: str, lines: list, encoding="utf-8") -> None:
477
- lines = [GeneralUtilities.strip_new_line_character(line) for line in lines]
478
- content = os.linesep.join(lines)
479
- GeneralUtilities.write_text_to_file(file, content, encoding)
480
-
481
- @staticmethod
482
- @check_arguments
483
- def write_text_to_file(file: str, content: str, encoding="utf-8") -> None:
484
- GeneralUtilities.write_binary_to_file(file, bytes(bytearray(content, encoding)))
485
-
486
- @staticmethod
487
- @check_arguments
488
- def write_binary_to_file(file: str, content: bytes) -> None:
489
- with open(file, "wb") as file_object:
490
- file_object.write(content)
491
-
492
- @staticmethod
493
- @check_arguments
494
- def read_lines_from_file(file: str, encoding="utf-8") -> list[str]:
495
- return [GeneralUtilities.strip_new_line_character(line) for line in GeneralUtilities.read_text_from_file(file, encoding).split('\n')]
496
-
497
- @staticmethod
498
- @check_arguments
499
- def read_text_from_file(file: str, encoding="utf-8") -> str:
500
- return GeneralUtilities.bytes_to_string(GeneralUtilities.read_binary_from_file(file), encoding)
501
-
502
- @staticmethod
503
- @check_arguments
504
- def read_binary_from_file(file: str) -> bytes:
505
- with open(file, "rb") as file_object:
506
- return file_object.read()
507
-
508
- @staticmethod
509
- @check_arguments
510
- def timedelta_to_simple_string(delta: timedelta) -> str:
511
- return (datetime(1970, 1, 1, 0, 0, 0) + delta).strftime('%H:%M:%S')
512
-
513
- @staticmethod
514
- @check_arguments
515
- def resolve_relative_path_from_current_working_directory(path: str) -> str:
516
- return GeneralUtilities.resolve_relative_path(path, os.getcwd())
517
-
518
- @staticmethod
519
- @check_arguments
520
- def resolve_relative_path(path: str, base_path: str):
521
- if (os.path.isabs(path)):
522
- return path
523
- else:
524
- return str(Path(os.path.join(base_path, path)).resolve())
525
-
526
- @staticmethod
527
- @check_arguments
528
- def get_metadata_for_file_for_clone_folder_structure(file: str) -> str:
529
- size = os.path.getsize(file)
530
- last_modified_timestamp = os.path.getmtime(file)
531
- hash_value = GeneralUtilities.get_sha256_of_file(file)
532
- last_access_timestamp = os.path.getatime(file)
533
- return f'{{"size":"{size}","sha256":"{hash_value}","mtime":"{last_modified_timestamp}","atime":"{last_access_timestamp}"}}'
534
-
535
- @staticmethod
536
- @check_arguments
537
- def clone_folder_structure(source: str, target: str, copy_only_metadata: bool):
538
- source = GeneralUtilities.resolve_relative_path(source, os.getcwd())
539
- target = GeneralUtilities.resolve_relative_path(target, os.getcwd())
540
- length_of_source = len(source)
541
- for source_file in GeneralUtilities.absolute_file_paths(source):
542
- target_file = target+source_file[length_of_source:]
543
- GeneralUtilities.ensure_directory_exists(os.path.dirname(target_file))
544
- if copy_only_metadata:
545
- with open(target_file, 'w', encoding='utf8') as f:
546
- f.write(GeneralUtilities.get_metadata_for_file_for_clone_folder_structure(source_file))
547
- else:
548
- copyfile(source_file, target_file)
549
-
550
- @staticmethod
551
- @check_arguments
552
- def current_user_has_elevated_privileges() -> bool:
553
- try:
554
- return os.getuid() == 0
555
- except AttributeError:
556
- return ctypes.windll.shell32.IsUserAnAdmin() == 1
557
-
558
- @staticmethod
559
- @check_arguments
560
- def ensure_elevated_privileges() -> None:
561
- if (not GeneralUtilities.current_user_has_elevated_privileges()):
562
- raise ValueError("Not enough privileges.")
563
-
564
- @staticmethod
565
- @check_arguments
566
- def rename_names_of_all_files_and_folders(folder: str, replace_from: str, replace_to: str, replace_only_full_match=False):
567
- for file in GeneralUtilities.get_direct_files_of_folder(folder):
568
- GeneralUtilities.replace_in_filename(file, replace_from, replace_to, replace_only_full_match)
569
- for sub_folder in GeneralUtilities.get_direct_folders_of_folder(folder):
570
- GeneralUtilities.rename_names_of_all_files_and_folders(sub_folder, replace_from, replace_to, replace_only_full_match)
571
- GeneralUtilities.replace_in_foldername(folder, replace_from, replace_to, replace_only_full_match)
572
-
573
- @staticmethod
574
- @check_arguments
575
- def get_direct_files_of_folder(folder: str) -> list[str]:
576
- result = [os.path.join(folder, f) for f in listdir(folder) if isfile(join(folder, f))]
577
- result = sorted(result, key=str.casefold)
578
- return result
579
-
580
- @staticmethod
581
- @check_arguments
582
- def get_direct_folders_of_folder(folder: str) -> list[str]:
583
- result = [os.path.join(folder, f) for f in listdir(folder) if isdir(join(folder, f))]
584
- result = sorted(result, key=str.casefold)
585
- return result
586
-
587
- @staticmethod
588
- @check_arguments
589
- def get_all_files_of_folder(folder: str) -> list[str]:
590
- result = list()
591
- result.extend(GeneralUtilities.get_direct_files_of_folder(folder))
592
- for subfolder in GeneralUtilities.get_direct_folders_of_folder(folder):
593
- result.extend(GeneralUtilities.get_all_files_of_folder(subfolder))
594
- result = sorted(result, key=str.casefold)
595
- return result
596
-
597
- @staticmethod
598
- @check_arguments
599
- def get_all_folders_of_folder(folder: str) -> list[str]:
600
- result = list()
601
- subfolders = GeneralUtilities.get_direct_folders_of_folder(folder)
602
- result.extend(subfolders)
603
- for subfolder in subfolders:
604
- result.extend(GeneralUtilities.get_all_folders_of_folder(subfolder))
605
- result = sorted(result, key=str.casefold)
606
- return result
607
-
608
- @staticmethod
609
- @check_arguments
610
- def get_all_objects_of_folder(folder: str) -> list[str]:
611
- return sorted(GeneralUtilities.get_all_files_of_folder(folder) + GeneralUtilities.get_all_folders_of_folder(folder), key=str.casefold)
612
-
613
- @staticmethod
614
- @check_arguments
615
- def replace_in_filename(file: str, replace_from: str, replace_to: str, replace_only_full_match=False):
616
- filename = Path(file).name
617
- if (GeneralUtilities.__should_get_replaced(filename, replace_from, replace_only_full_match)):
618
- folder_of_file = os.path.dirname(file)
619
- os.rename(file, os.path.join(folder_of_file, filename.replace(replace_from, replace_to)))
620
-
621
- @staticmethod
622
- @check_arguments
623
- def replace_in_foldername(folder: str, replace_from: str, replace_to: str, replace_only_full_match=False):
624
- foldername = Path(folder).name
625
- if (GeneralUtilities.__should_get_replaced(foldername, replace_from, replace_only_full_match)):
626
- folder_of_folder = os.path.dirname(folder)
627
- os.rename(folder, os.path.join(folder_of_folder, foldername.replace(replace_from, replace_to)))
628
-
629
- @staticmethod
630
- @check_arguments
631
- def __should_get_replaced(input_text, search_text, replace_only_full_match) -> bool:
632
- if replace_only_full_match:
633
- return input_text == search_text
634
- else:
635
- return search_text in input_text
636
-
637
- @staticmethod
638
- @check_arguments
639
- def str_none_safe(variable) -> str:
640
- if variable is None:
641
- return ''
642
- else:
643
- return str(variable)
644
-
645
- @staticmethod
646
- @check_arguments
647
- def arguments_to_array(arguments_as_string: str) -> list[str]:
648
- if arguments_as_string is None:
649
- return []
650
- if GeneralUtilities.string_has_content(arguments_as_string):
651
- return arguments_as_string.split(" ") # TODO this function should get improved to allow whitespaces in quote-substrings
652
- else:
653
- return []
654
-
655
- @staticmethod
656
- @check_arguments
657
- def arguments_to_array_for_log(arguments_as_string: str) -> list[str]:
658
- if arguments_as_string is None:
659
- return None
660
- return GeneralUtilities.arguments_to_array(arguments_as_string)
661
-
662
- @staticmethod
663
- @check_arguments
664
- def get_sha256_of_file(file: str) -> str:
665
- sha256 = hashlib.sha256()
666
- with open(file, "rb") as fileObject:
667
- for chunk in iter(lambda: fileObject.read(4096), b""):
668
- sha256.update(chunk)
669
- return sha256.hexdigest()
670
-
671
- @staticmethod
672
- @check_arguments
673
- def remove_duplicates(input_list) -> list:
674
- result = []
675
- for item in input_list:
676
- if not item in result:
677
- result.append(item)
678
- return result
679
-
680
- @staticmethod
681
- @check_arguments
682
- def print_stacktrace() -> None:
683
- for line in traceback.format_stack():
684
- GeneralUtilities.write_message_to_stdout(line.strip())
685
-
686
- @staticmethod
687
- @check_arguments
688
- def string_to_boolean(value: str) -> bool:
689
- value = value.strip().lower()
690
- if value in ('yes', 'y', 'true', 't', '1'):
691
- return True
692
- elif value in ('no', 'n', 'false', 'f', '0'):
693
- return False
694
- else:
695
- raise ValueError(f"Can not convert '{value}' to a boolean value")
696
-
697
- @staticmethod
698
- @check_arguments
699
- def file_is_empty(file: str) -> bool:
700
- return os.stat(file).st_size == 0
701
-
702
- @staticmethod
703
- @check_arguments
704
- def folder_is_empty(folder: str) -> bool:
705
- return len(GeneralUtilities.get_direct_files_of_folder(folder)) == 0 and len(GeneralUtilities.get_direct_folders_of_folder(folder)) == 0
706
-
707
- @staticmethod
708
- @check_arguments
709
- def get_time_based_logfile_by_folder(folder: str, name: str = "Log", in_utc: bool = False) -> str:
710
- return os.path.join(GeneralUtilities.resolve_relative_path_from_current_working_directory(folder), f"{GeneralUtilities.get_time_based_logfilename(name, in_utc)}.log")
711
-
712
- @staticmethod
713
- @check_arguments
714
- def get_time_based_logfilename(name: str = "Log", in_utc: bool = False) -> str:
715
- if (in_utc):
716
- d = datetime.utcnow()
717
- else:
718
- d = datetime.now()
719
- return f"{name}_{GeneralUtilities.datetime_to_string_for_logfile_name(d)}"
720
-
721
- @staticmethod
722
- @check_arguments
723
- def bytes_to_string(payload: bytes, encoding: str = 'utf-8') -> str:
724
- return payload.decode(encoding, errors="ignore")
725
-
726
- @staticmethod
727
- @check_arguments
728
- def string_to_bytes(payload: str, encoding: str = 'utf-8') -> bytes:
729
- return payload.encode(encoding, errors="ignore")
730
-
731
- @staticmethod
732
- @check_arguments
733
- def contains_line(lines, regex: str) -> bool:
734
- for line in lines:
735
- if (re.match(regex, line)):
736
- return True
737
- return False
738
-
739
- @staticmethod
740
- @check_arguments
741
- 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,
742
- encoding="utf-8", ignore_empty_lines: bool = True, separator_character: str = ";", values_are_surrounded_by_quotes: bool = False) -> list[list[str]]:
743
- lines = GeneralUtilities.read_lines_from_file(file, encoding)
744
-
745
- if ignore_first_line:
746
- lines = lines[1:]
747
- result = list()
748
- line: str
749
- for line_loopvariable in lines:
750
- use_line = True
751
- line = line_loopvariable
752
-
753
- if trim_values:
754
- line = line.strip()
755
- if ignore_empty_lines:
756
- if not GeneralUtilities.string_has_content(line):
757
- use_line = False
758
-
759
- if treat_number_sign_at_begin_of_line_as_comment:
760
- if line.startswith("#"):
761
- use_line = False
762
-
763
- if use_line:
764
- if separator_character in line:
765
- raw_values_of_line = GeneralUtilities.to_list(line, separator_character)
766
- else:
767
- raw_values_of_line = [line]
768
- if trim_values:
769
- raw_values_of_line = [value.strip() for value in raw_values_of_line]
770
- values_of_line = []
771
- for raw_value_of_line in raw_values_of_line:
772
- value_of_line = raw_value_of_line
773
- if values_are_surrounded_by_quotes:
774
- value_of_line = value_of_line[1:]
775
- value_of_line = value_of_line[:-1]
776
- value_of_line = value_of_line.replace('""', '"')
777
- values_of_line.append(value_of_line)
778
- result.extend([values_of_line])
779
- return result
780
-
781
- @staticmethod
782
- @check_arguments
783
- def epew_is_available() -> bool:
784
- try:
785
- return shutil.which("epew") is not None
786
- except:
787
- return False
788
-
789
- @staticmethod
790
- @check_arguments
791
- @deprecated
792
- def absolute_file_paths(directory: str) -> list[str]:
793
- return GeneralUtilities.get_all_files_of_folder(directory)
794
-
795
- @staticmethod
796
- @check_arguments
797
- def to_list(list_as_string: str, separator: str = ",") -> list[str]:
798
- result = list()
799
- if list_as_string is not None:
800
- list_as_string = list_as_string.strip()
801
- if list_as_string == "":
802
- pass
803
- elif separator in list_as_string:
804
- for item in list_as_string.split(separator):
805
- result.append(item.strip())
806
- else:
807
- result.append(list_as_string)
808
- return result
809
-
810
- @staticmethod
811
- @check_arguments
812
- def get_next_square_number(number: int) -> int:
813
- GeneralUtilities.assert_condition(number >= 0, "get_next_square_number is only applicable for nonnegative numbers")
814
- if number == 0:
815
- return 1
816
- root = 0
817
- square = 0
818
- while square < number:
819
- root = root+1
820
- square = root*root
821
- return root*root
822
-
823
- @staticmethod
824
- @check_arguments
825
- def generate_password(length: int = 16, alphabet: str = None) -> None:
826
- if alphabet is None:
827
- alphabet = strin.ascii_letters + strin.digits+"_"
828
- return ''.join(secrets.choice(alphabet) for i in range(length))
829
-
830
- @staticmethod
831
- @check_arguments
832
- def assert_condition(condition: bool, information: str) -> None:
833
- if (not condition):
834
- raise ValueError("Condition failed. "+information)
835
-
836
- @staticmethod
837
- def current_system_is_windows():
838
- return platform.system() == 'Windows'
839
-
840
- @staticmethod
841
- def current_system_is_linux():
842
- return platform.system() == 'Linux'
843
-
844
- @staticmethod
845
- @check_arguments
846
- def get_certificate_expiry_date(certificate_file: str) -> datetime:
847
- with open(certificate_file, encoding="utf-8") as certificate_file_content:
848
- cert = crypto.load_certificate(crypto.FILETYPE_PEM, certificate_file_content.read())
849
- date_as_bytes = cert.get_notAfter()
850
- date_as_string = date_as_bytes.decode("utf-8")
851
- result = datetime.strptime(date_as_string, '%Y%m%d%H%M%SZ')
852
- return result
853
-
854
- @staticmethod
855
- @check_arguments
856
- def certificate_is_expired(certificate_file: str) -> bool:
857
- return GeneralUtilities.get_certificate_expiry_date(certificate_file) < datetime.now()
858
-
859
- @staticmethod
860
- @check_arguments
861
- def internet_connection_is_available() -> bool:
862
- # TODO add more hosts to check to return true if at least one is available
863
- try:
864
- with urllib.request.urlopen("https://google.com") as url_result:
865
- return (url_result.code // 100) == 2
866
- except:
867
- pass
868
- return False
869
-
870
- @staticmethod
871
- @check_arguments
872
- def replace_variable_in_string(input_string: str, variable_name: str, variable_value: str) -> None:
873
- return input_string.replace(f"__[{variable_name}]__", variable_value)
1
+ import re
2
+ import os
3
+ from os import listdir
4
+ from os.path import isfile, join, isdir
5
+ import codecs
6
+ import platform
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 enum import Enum
20
+ import traceback
21
+ import warnings
22
+ import functools
23
+ from pathlib import Path
24
+ from datetime import datetime, timedelta, date, timezone
25
+ import typing
26
+ from packaging.version import Version
27
+ import psutil
28
+ from defusedxml.minidom import parse
29
+ from OpenSSL import crypto
30
+
31
+ class VersionEcholon(Enum):
32
+ LatestPatch = 0
33
+ LatestPatchOrLatestMinor = 1
34
+ LatestPatchOrLatestMinorOrNextMajor = 2
35
+ LatestVersion = 3
36
+
37
+
38
+ class Dependency:
39
+ dependencyname:str
40
+ current_version:str
41
+
42
+ def __init__(self,dependencyname:str,current_version:str):
43
+ self.dependencyname=dependencyname
44
+ self.current_version=current_version
45
+
46
+ class GeneralUtilities:
47
+
48
+ __datetime_format_with_offset: str = "%Y-%m-%d %H:%M:%S %z"
49
+ __datetime_format: str = "%Y-%m-%dT%H:%M:%S"
50
+ __date_format: str = "%Y-%m-%d"
51
+
52
+ empty_string: str = ""
53
+
54
+ @staticmethod
55
+ def get_modest_dark_url() -> str:
56
+ return "https://aniondev.github.io/CDN/ScriptCollectionDesigns/ModestDark/Style.css"
57
+
58
+ @staticmethod
59
+ def is_generic(t: typing.Type):
60
+ return hasattr(t, "__origin__")
61
+
62
+ @staticmethod
63
+ def is_debugger_attached():
64
+ return sys.gettrace() is not None
65
+
66
+ @staticmethod
67
+ def check_arguments(function):
68
+ def __check_function(*args, **named_args):
69
+ parameters: list = inspect.getfullargspec(function)[0].copy()
70
+ arguments: list = list(tuple(args)).copy()
71
+ if "self" in parameters:
72
+ parameters.remove("self")
73
+ arguments.pop(0)
74
+ for index, argument in enumerate(arguments):
75
+ if argument is not None: # Check type of None is not possible. None is always a valid argument-value
76
+ if parameters[index] in function.__annotations__: # Check if a type-hint for parameter exist. If not, no parameter-type available for argument-type-check
77
+ # Check type of arguments if the type is a generic type seems to be impossible.
78
+ if not GeneralUtilities.is_generic(function.__annotations__[parameters[index]]):
79
+ if not isinstance(argument, function.__annotations__[parameters[index]]):
80
+ 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)))
81
+ for index, named_argument in enumerate(named_args):
82
+ if named_args[named_argument] is not None:
83
+ if parameters[index] in function.__annotations__:
84
+ if not GeneralUtilities.is_generic(function.__annotations__.get(named_argument)):
85
+ if not isinstance(named_args[named_argument], function.__annotations__.get(named_argument)):
86
+ 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)}")
87
+ return function(*args, **named_args)
88
+ __check_function.__doc__ = function.__doc__
89
+ return __check_function
90
+
91
+ @staticmethod
92
+ def deprecated(func):
93
+ @functools.wraps(func)
94
+ def new_func(*args, **kwargs):
95
+ warnings.simplefilter('always', DeprecationWarning)
96
+ warnings.warn(f"Call to deprecated function {func.__name__}", category=DeprecationWarning, stacklevel=2)
97
+ warnings.simplefilter('default', DeprecationWarning)
98
+ return func(*args, **kwargs)
99
+ return new_func
100
+
101
+ @staticmethod
102
+ @check_arguments
103
+ def args_array_surround_with_quotes_if_required(arguments: list[str]) -> list[str]:
104
+ result = []
105
+ for argument in arguments:
106
+ if " " in argument and not (argument.startswith('"') and argument.endswith('"')):
107
+ result.append(f'"{argument}"')
108
+ else:
109
+ result.append(argument)
110
+ return result
111
+
112
+ @staticmethod
113
+ @check_arguments
114
+ def string_to_lines(string: str, add_empty_lines: bool = True, adapt_lines: bool = True) -> list[str]:
115
+ result : list[str] = list[str]()
116
+ lines : list[str] = list[str]()
117
+ if (string is not None):
118
+ if ("\n" in string):
119
+ lines = string.split("\n")
120
+ else:
121
+ lines.append(string)
122
+ for rawline in lines:
123
+ if adapt_lines:
124
+ line = rawline.replace("\r", "").strip()
125
+ else:
126
+ line = rawline
127
+ if GeneralUtilities.string_is_none_or_whitespace(line):
128
+ if add_empty_lines:
129
+ result.append(line)
130
+ else:
131
+ result.append(line)
132
+ return result
133
+
134
+ @staticmethod
135
+ @check_arguments
136
+ def string_to_datetime(value: str) -> datetime:
137
+ if "." in value:
138
+ value = value.split(".")[0]
139
+ return datetime.strptime(value, GeneralUtilities.__datetime_format) # value ="2022-10-06T19:26:01" for example
140
+
141
+ @staticmethod
142
+ @check_arguments
143
+ def datetime_to_string(value: datetime) -> str:
144
+ value = datetime(year=value.year, month=value.month, day=value.day, hour=value.hour, minute=value.minute, second=value.second)
145
+ return value.strftime(GeneralUtilities.__datetime_format) # returns "2022-10-06T19:26:01" for example
146
+
147
+ @staticmethod
148
+ @check_arguments
149
+ def datetime_to_string_with_timezone(value: datetime) -> str:
150
+ return value.strftime(GeneralUtilities.__datetime_format_with_offset) # returns "2025-08-21 15:30:00 +0200" for example
151
+
152
+ @staticmethod
153
+ @check_arguments
154
+ def string_to_date(value: str) -> date:
155
+ splitted = value.split("-")
156
+ return date(int(splitted[0]), int(splitted[1]), int(splitted[2])) # value ="2022-10-06" for example
157
+
158
+ @staticmethod
159
+ @check_arguments
160
+ def date_to_string(value: date) -> str:
161
+ return value.strftime(GeneralUtilities.__date_format) # returns "2022-10-06" for example
162
+
163
+ @staticmethod
164
+ @check_arguments
165
+ def copy_content_of_folder(source_directory: str, target_directory: str, overwrite_existing_files=False, ignored_glob_patterms: list[str] = None) -> None:
166
+ GeneralUtilities.__copy_or_move_content_of_folder(source_directory, target_directory, overwrite_existing_files, False, ignored_glob_patterms)
167
+
168
+ @staticmethod
169
+ @check_arguments
170
+ def move_content_of_folder(source_directory: str, target_directory: str, overwrite_existing_files=False, ignored_glob_patterms: list[str] = None) -> None:
171
+ GeneralUtilities.__copy_or_move_content_of_folder(source_directory, target_directory, overwrite_existing_files, True, ignored_glob_patterms)
172
+
173
+ @staticmethod
174
+ @check_arguments
175
+ def merge_dependency_lists(versions:list[list[Dependency]]) -> dict[str,set[str]]:
176
+ result:dict[str,set[str]]=dict[str,set[str]]()
177
+ for dlist in versions:
178
+ for ditem in dlist:
179
+ if not ditem.dependencyname in result:
180
+ result[ditem.dependencyname]=set[str]()
181
+ result[ditem.dependencyname].add(ditem.current_version)
182
+ return result
183
+
184
+ @staticmethod
185
+ @check_arguments
186
+ def choose_version(available_versions:list[str],current_version:str,echolon:VersionEcholon) -> str:
187
+ match echolon:
188
+ case VersionEcholon.LatestPatch:
189
+ 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)}."))
190
+ case VersionEcholon.LatestPatchOrLatestMinor:
191
+ return GeneralUtilities.get_latest_version(GeneralUtilities.filter_versions_by_prefix(available_versions,f"{GeneralUtilities.get_major_part_of_version(current_version)}."))
192
+ case VersionEcholon.LatestPatchOrLatestMinorOrNextMajor:
193
+ raise ValueError("not implemented")#TODO
194
+ case VersionEcholon.LatestVersion:
195
+ return GeneralUtilities.get_latest_version(available_versions)
196
+ case _:
197
+ raise ValueError("Unknown echolon-value: "+str(echolon))
198
+
199
+ @staticmethod
200
+ @check_arguments
201
+ def get_latest_version(versions:list[str]) -> str:
202
+ GeneralUtilities.assert_condition(0<len(versions),"Version-list can not be empty.")
203
+ latest = max(versions, key=Version)
204
+ return latest
205
+
206
+ @staticmethod
207
+ @check_arguments
208
+ def filter_versions_by_prefix(versions:list[str],prefix:str) -> list[str]:
209
+ return [v for v in versions if v.startswith(prefix)]
210
+
211
+ @staticmethod
212
+ @check_arguments
213
+ def get_major_part_of_version(version:str) -> int:
214
+ return GeneralUtilities.get_version_parts(version)[0]
215
+
216
+ @staticmethod
217
+ @check_arguments
218
+ def get_minor_part_of_version(version:str) -> int:
219
+ return GeneralUtilities.get_version_parts(version)[1]
220
+
221
+ @staticmethod
222
+ @check_arguments
223
+ def get_patch_part_of_version(version:str) -> int:
224
+ return GeneralUtilities.get_version_parts(version)[2]
225
+
226
+ @staticmethod
227
+ @check_arguments
228
+ def get_version_parts(version:str) -> tuple[int,int,int]:
229
+ match = re.match(r"^(\d+).(\d+).(\d+)$", version)
230
+ GeneralUtilities.assert_condition(match is not None,f"string \"{version}\" is not a valid version.")
231
+ return (int(match.group(1)),int(match.group(2)),int(match.group(3)))
232
+
233
+ @staticmethod
234
+ @check_arguments
235
+ def is_ignored_by_glob_pattern(source_directory:str,path:str, ignored_glob_patterms: list[str]) -> bool:
236
+ source_directory=source_directory.replace("\\","/")
237
+ path=path.replace("\\","/")
238
+ GeneralUtilities.assert_condition(path.startswith(source_directory), f"Path '{path}' is not located in source directory '{source_directory}'.")
239
+ if ignored_glob_patterms is None:
240
+ return False
241
+ relative_path = os.path.relpath(path, source_directory)
242
+ for pattern in ignored_glob_patterms:
243
+ if fnmatch.filter([relative_path], pattern):
244
+ return True
245
+ return False
246
+
247
+
248
+ @staticmethod
249
+ @check_arguments
250
+ 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:
251
+ srcDirFull = GeneralUtilities.resolve_relative_path_from_current_working_directory(source_directory)
252
+ dstDirFull = GeneralUtilities.resolve_relative_path_from_current_working_directory(target_directory)
253
+ if (os.path.isdir(source_directory)):
254
+ GeneralUtilities.ensure_directory_exists(target_directory)
255
+ for file in GeneralUtilities.get_direct_files_of_folder(srcDirFull):
256
+ filename = os.path.basename(file)
257
+ if not GeneralUtilities.is_ignored_by_glob_pattern(source_directory,file, ignored_glob_patterms):
258
+ targetfile = os.path.join(dstDirFull, filename)
259
+ if (os.path.isfile(targetfile)):
260
+ if overwrite_existing_files:
261
+ GeneralUtilities.ensure_file_does_not_exist(targetfile)
262
+ else:
263
+ raise ValueError(f"Targetfile '{targetfile}' does already exist.")
264
+ if remove_source:
265
+ shutil.move(file, dstDirFull)
266
+ else:
267
+ shutil.copy(file, dstDirFull)
268
+ for sub_folder in GeneralUtilities.get_direct_folders_of_folder(srcDirFull):
269
+ if not GeneralUtilities.is_ignored_by_glob_pattern(source_directory,sub_folder, ignored_glob_patterms):
270
+ foldername = os.path.basename(sub_folder)
271
+ sub_target = os.path.join(dstDirFull, foldername)
272
+ GeneralUtilities.__copy_or_move_content_of_folder(sub_folder, sub_target, overwrite_existing_files, remove_source,ignored_glob_patterms)
273
+ if remove_source:
274
+ GeneralUtilities.ensure_directory_does_not_exist(sub_folder)
275
+ else:
276
+ raise ValueError(f"Folder '{source_directory}' does not exist")
277
+
278
+ @staticmethod
279
+ @check_arguments
280
+ def replace_regex_each_line_of_file(file: str, replace_from_regex: str, replace_to_regex: str, encoding="utf-8", verbose: bool = False) -> None:
281
+ if verbose:
282
+ GeneralUtilities.write_message_to_stdout(f"Replace '{replace_from_regex}' to '{replace_to_regex}' in '{file}'")
283
+ lines=GeneralUtilities.read_lines_from_file(file,encoding)
284
+ replaced_lines = []
285
+ for line in lines:
286
+ replaced_line = re.sub(replace_from_regex, replace_to_regex, line)
287
+ replaced_lines.append(replaced_line)
288
+ GeneralUtilities.write_lines_to_file(file,replaced_lines,encoding)
289
+
290
+ @staticmethod
291
+ @check_arguments
292
+ def replace_regex_in_file(file: str, replace_from_regex: str, replace_to_regex: str, encoding="utf-8") -> None:
293
+ with open(file, encoding=encoding, mode="r") as f:
294
+ content = f.read()
295
+ content = re.sub(replace_from_regex, replace_to_regex, content)
296
+ with open(file, encoding=encoding, mode="w") as f:
297
+ f.write(content)
298
+
299
+ @staticmethod
300
+ @check_arguments
301
+ def replace_xmltag_in_file(file: str, tag: str, new_value: str, encoding="utf-8") -> None:
302
+ GeneralUtilities.assert_condition(tag.isalnum(tag), f"Invalid tag: \"{tag}\"")
303
+ GeneralUtilities.replace_regex_in_file(file, f"<{tag}>.*</{tag}>", f"<{tag}>{new_value}</{tag}>", encoding)
304
+
305
+ @staticmethod
306
+ @check_arguments
307
+ def update_version_in_csproj_file(file: str, target_version: str) -> None:
308
+ GeneralUtilities.replace_xmltag_in_file(file, "Version", target_version)
309
+ GeneralUtilities.replace_xmltag_in_file(file, "AssemblyVersion", target_version + ".0")
310
+ GeneralUtilities.replace_xmltag_in_file(file, "FileVersion", target_version + ".0")
311
+
312
+ @staticmethod
313
+ @check_arguments
314
+ def replace_underscores_in_text(text: str, replacements: dict) -> str:
315
+ changed = True
316
+ while changed:
317
+ changed = False
318
+ for key, value in replacements.items():
319
+ previousValue = text
320
+ text = text.replace(f"__{key}__", value)
321
+ if (not text == previousValue):
322
+ changed = True
323
+ return text
324
+
325
+ @staticmethod
326
+ @check_arguments
327
+ def replace_underscores_in_file(file: str, replacements: dict, encoding: str = "utf-8"):
328
+ text = GeneralUtilities.read_text_from_file(file, encoding)
329
+ text = GeneralUtilities.replace_underscores_in_text(text, replacements)
330
+ GeneralUtilities.write_text_to_file(file, text, encoding)
331
+
332
+ @staticmethod
333
+ @check_arguments
334
+ def print_text(text: str, print_to_stdout: bool = True):
335
+ stream: object = sys.stdout if print_to_stdout else sys.stderr
336
+ GeneralUtilities.__print_text_to_console(text, stream)
337
+
338
+ @staticmethod
339
+ @check_arguments
340
+ def print_text_in_green(text: str, print_to_stdout: bool = True, print_as_color: bool = True):
341
+ GeneralUtilities.print_text_in_color(text, 32, print_to_stdout, print_as_color)
342
+
343
+ @staticmethod
344
+ @check_arguments
345
+ def print_text_in_yellow(text: str, print_to_stdout: bool = True, print_as_color: bool = True):
346
+ GeneralUtilities.print_text_in_color(text, 33, print_to_stdout, print_as_color)
347
+
348
+ @staticmethod
349
+ @check_arguments
350
+ def print_text_in_red(text: str, print_to_stdout: bool = True, print_as_color: bool = True):
351
+ GeneralUtilities.print_text_in_color(text, 31, print_to_stdout, print_as_color)
352
+
353
+ @staticmethod
354
+ @check_arguments
355
+ def print_text_in_cyan(text: str, print_to_stdout: bool = True, print_as_color: bool = True):
356
+ GeneralUtilities.print_text_in_color(text, 36, print_to_stdout, print_as_color)
357
+
358
+ @staticmethod
359
+ @check_arguments
360
+ def print_text_in_color(text: str, colorcode: int, print_to_stdout: bool = True, print_as_color: bool = True):
361
+ stream: object = sys.stdout if print_to_stdout else sys.stderr
362
+ if print_as_color:
363
+ text = f"\033[{colorcode}m{text}\033[0m"
364
+ GeneralUtilities.__print_text_to_console(text, stream)
365
+
366
+ @staticmethod
367
+ @check_arguments
368
+ def __print_text_to_console(text: str, stream: object):
369
+ stream.write(text)
370
+ stream.flush()
371
+
372
+ @staticmethod
373
+ @check_arguments
374
+ def reconfigure_standrd_input_and_outputs():
375
+ sys.stdin.reconfigure(encoding='utf-8')
376
+ sys.stderr.reconfigure(encoding='utf-8')
377
+ sys.stdout.reconfigure(encoding='utf-8')
378
+
379
+ @staticmethod
380
+ @check_arguments
381
+ def write_message_to_stdout_advanced(message: str, add_empty_lines: bool = True, adapt_lines: bool = True, append_linebreak: bool = True):
382
+ new_line_character: str = "\n" if append_linebreak else GeneralUtilities.empty_string
383
+ for line in GeneralUtilities.string_to_lines(message, add_empty_lines, adapt_lines):
384
+ sys.stdout.write(GeneralUtilities.str_none_safe(line)+new_line_character)
385
+ sys.stdout.flush()
386
+
387
+ @staticmethod
388
+ @check_arguments
389
+ def write_message_to_stdout(message: str):
390
+ GeneralUtilities.write_message_to_stdout_advanced(message, True, True, True)
391
+
392
+ @staticmethod
393
+ @check_arguments
394
+ def write_message_to_stderr_advanced(message: str, add_empty_lines: bool = True, adapt_lines: bool = True, append_linebreak: bool = True):
395
+ new_line_character: str = "\n" if append_linebreak else GeneralUtilities.empty_string
396
+ for line in GeneralUtilities.string_to_lines(message, add_empty_lines, adapt_lines):
397
+ sys.stderr.write(GeneralUtilities.str_none_safe(line)+new_line_character)
398
+ sys.stderr.flush()
399
+
400
+ @staticmethod
401
+ @check_arguments
402
+ def write_message_to_stderr(message: str):
403
+ GeneralUtilities.write_message_to_stderr_advanced(message, True, True, True)
404
+
405
+ @staticmethod
406
+ @check_arguments
407
+ def get_advanced_errormessage_for_os_error(os_error: OSError) -> str:
408
+ if GeneralUtilities.string_has_content(os_error.filename2):
409
+ secondpath = f" {os_error.filename2}"
410
+ else:
411
+ secondpath = GeneralUtilities.empty_string
412
+ return f"Related path(s): {os_error.filename}{secondpath}"
413
+
414
+ @staticmethod
415
+ @check_arguments
416
+ def write_exception_to_stderr(exception: Exception, extra_message: str = None):
417
+ GeneralUtilities.write_exception_to_stderr_with_traceback(exception, None, extra_message)
418
+
419
+ @staticmethod
420
+ @check_arguments
421
+ def write_exception_to_stderr_with_traceback(exception: Exception, current_traceback=None, extra_message: str = None):
422
+ GeneralUtilities.write_message_to_stderr("Exception(")
423
+ GeneralUtilities.write_message_to_stderr("Type: " + str(type(exception)))
424
+ GeneralUtilities.write_message_to_stderr("Message: " + str(exception))
425
+ if extra_message is not None:
426
+ GeneralUtilities.write_message_to_stderr("Extra-message: " + str(extra_message))
427
+ if isinstance(exception, OSError):
428
+ GeneralUtilities.write_message_to_stderr(GeneralUtilities.get_advanced_errormessage_for_os_error(exception))
429
+ if current_traceback is not None:
430
+ GeneralUtilities.write_message_to_stderr("Traceback: " + current_traceback.format_exc())
431
+ GeneralUtilities.write_message_to_stderr(")")
432
+
433
+ @staticmethod
434
+ @check_arguments
435
+ def string_has_content(string: str) -> bool:
436
+ if string is None:
437
+ return False
438
+ else:
439
+ return len(string.strip()) > 0
440
+
441
+ @staticmethod
442
+ @check_arguments
443
+ def datetime_to_string_for_logfile_name(datetime_object: datetime, add_timezone_info_to_log: bool = True) -> str:
444
+ base_pattern: str = "%Y-%m-%d_%H-%M-%S"
445
+ if add_timezone_info_to_log:
446
+ return datetime_object.strftime(f'{base_pattern}_%z')
447
+ else:
448
+ return datetime_object.strftime(base_pattern)
449
+
450
+ @staticmethod
451
+ @check_arguments
452
+ def datetime_to_string_for_logfile_entry(datetime_object: datetime, add_milliseconds: bool = False) -> str:
453
+ if datetime_object.tzinfo is None:
454
+ datetime_object = datetime_object.replace(tzinfo=timezone.utc) # assume utc when no timezone is given
455
+ pattern: str = None
456
+ if add_milliseconds:
457
+ pattern = "%Y-%m-%dT%H:%M:%S.%f%z"
458
+ else:
459
+ pattern = "%Y-%m-%dT%H:%M:%S%z"
460
+ s = datetime_object.strftime(pattern)
461
+ s = s[:-2] + ":" + s[-2:]
462
+ return s
463
+
464
+ @staticmethod
465
+ @check_arguments
466
+ def string_has_nonwhitespace_content(string: str) -> bool:
467
+ if string is None:
468
+ return False
469
+ else:
470
+ return len(string.strip()) > 0
471
+
472
+ @staticmethod
473
+ @check_arguments
474
+ def string_is_none_or_empty(argument: str) -> bool:
475
+ if argument is None:
476
+ return True
477
+ type_of_argument = type(argument)
478
+ if type_of_argument == str:
479
+ return argument == GeneralUtilities.empty_string
480
+ else:
481
+ raise ValueError(f"expected string-variable in argument of string_is_none_or_empty but the type was '{str(type_of_argument)}'")
482
+
483
+ @staticmethod
484
+ @check_arguments
485
+ def string_is_none_or_whitespace(string: str) -> bool:
486
+ if GeneralUtilities.string_is_none_or_empty(string):
487
+ return True
488
+ else:
489
+ return string.strip() == GeneralUtilities.empty_string
490
+
491
+ @staticmethod
492
+ @check_arguments
493
+ def strip_new_line_character(value: str) -> str:
494
+ while not GeneralUtilities.__strip_new_line_character_helper_value_is_ok(value):
495
+ value = GeneralUtilities.__strip_new_line_character_helper_normalize_value(value)
496
+ return value
497
+
498
+ @staticmethod
499
+ @check_arguments
500
+ def __strip_new_line_character_helper_value_is_ok(value: str) -> bool:
501
+ if value.startswith("\r") or value.endswith("\r"):
502
+ return False
503
+ if value.startswith("\n") or value.endswith("\n"):
504
+ return False
505
+ return True
506
+
507
+ @staticmethod
508
+ @check_arguments
509
+ def __strip_new_line_character_helper_normalize_value(value: str) -> str:
510
+ return value.strip('\n').strip('\r')
511
+
512
+ @staticmethod
513
+ @check_arguments
514
+ def file_ends_with_newline(file: str) -> bool:
515
+ with open(file, "rb") as file_object:
516
+ return GeneralUtilities.ends_with_newline_character(file_object.read())
517
+
518
+ @staticmethod
519
+ @check_arguments
520
+ def ends_with_newline_character(content: bytes) -> bool:
521
+ result = content.endswith(GeneralUtilities.string_to_bytes("\n"))
522
+ return result
523
+
524
+ @staticmethod
525
+ @check_arguments
526
+ def file_ends_with_content(file: str) -> bool:
527
+ content = GeneralUtilities.read_binary_from_file(file)
528
+ if len(content) == 0:
529
+ return False
530
+ else:
531
+ if GeneralUtilities.ends_with_newline_character(content):
532
+ return False
533
+ else:
534
+ return True
535
+
536
+ @staticmethod
537
+ @check_arguments
538
+ def get_new_line_character_for_textfile_if_required(file: str) -> bool:
539
+ if GeneralUtilities.file_ends_with_content(file):
540
+ return "\n"
541
+ else:
542
+ return GeneralUtilities.empty_string
543
+
544
+ @staticmethod
545
+ @check_arguments
546
+ def append_line_to_file(file: str, line: str, encoding: str = "utf-8") -> None:
547
+ GeneralUtilities.append_lines_to_file(file, [line], encoding)
548
+
549
+ @staticmethod
550
+ @check_arguments
551
+ def append_lines_to_file(file: str, lines: list[str], encoding: str = "utf-8") -> None:
552
+ if len(lines) == 0:
553
+ return
554
+ is_first_line = True
555
+ for line in lines:
556
+ insert_linebreak: bool
557
+ if is_first_line:
558
+ insert_linebreak = GeneralUtilities.file_ends_with_content(file)
559
+ else:
560
+ insert_linebreak = True
561
+ line_to_write: str = None
562
+ if insert_linebreak:
563
+ line_to_write = "\n"+line
564
+ else:
565
+ line_to_write = line
566
+ with open(file, "r+b") as fileObject:
567
+ fileObject.seek(0, os.SEEK_END)
568
+ fileObject.write(GeneralUtilities.string_to_bytes(line_to_write, encoding))
569
+ is_first_line = False
570
+
571
+ @staticmethod
572
+ @check_arguments
573
+ def append_to_file(file: str, content: str, encoding: str = "utf-8") -> None:
574
+ GeneralUtilities.assert_condition(not "\n" in content, "Appending multiple lines is not allowed. Use append_lines_to_file instead.")
575
+ with open(file, "a", encoding=encoding) as fileObject:
576
+ fileObject.write(content)
577
+
578
+ @staticmethod
579
+ @check_arguments
580
+ def ensure_directory_exists(path: str) -> None:
581
+ if not os.path.isdir(path):
582
+ os.makedirs(path)
583
+
584
+ @staticmethod
585
+ @check_arguments
586
+ def ensure_file_exists(path: str) -> None:
587
+ if (not os.path.isfile(path)):
588
+ with open(path, "a+", encoding="utf-8"):
589
+ pass
590
+
591
+ @staticmethod
592
+ @check_arguments
593
+ def __remove_readonly(func, path, _):
594
+ os.chmod(path, stat.S_IWRITE)
595
+ func(path)
596
+
597
+ @staticmethod
598
+ @check_arguments
599
+ def __rmtree(directory: str) -> None:
600
+ shutil.rmtree(directory, onerror=GeneralUtilities.__remove_readonly) # pylint: disable=deprecated-argument
601
+
602
+ @staticmethod
603
+ @check_arguments
604
+ def ensure_directory_does_not_exist(path: str) -> None:
605
+ if (os.path.isdir(path)):
606
+ for root, dirs, files in os.walk(path, topdown=False):
607
+ for name in files:
608
+ filename = os.path.join(root, name)
609
+ os.chmod(filename, stat.S_IWUSR)
610
+ os.remove(filename)
611
+ for name in dirs:
612
+ GeneralUtilities.__rmtree(os.path.join(root, name))
613
+ GeneralUtilities.__rmtree(path)
614
+
615
+ @staticmethod
616
+ @check_arguments
617
+ def ensure_folder_exists_and_is_empty(path: str) -> None:
618
+ GeneralUtilities.ensure_directory_exists(path)
619
+ for filename in os.listdir(path):
620
+ file_path = os.path.join(path, filename)
621
+ if os.path.isfile(file_path):
622
+ os.remove(file_path)
623
+ if os.path.islink(file_path):
624
+ os.unlink(file_path)
625
+ elif os.path.isdir(file_path):
626
+ shutil.rmtree(file_path)
627
+
628
+ @staticmethod
629
+ @check_arguments
630
+ def ensure_file_does_not_exist(path: str) -> None:
631
+ if (os.path.isfile(path)):
632
+ os.remove(path)
633
+
634
+ @staticmethod
635
+ @check_arguments
636
+ def ensure_path_does_not_exist(path: str) -> None:
637
+ if (os.path.isfile(path)):
638
+ GeneralUtilities.ensure_file_does_not_exist(path)
639
+ if (os.path.isdir(path)):
640
+ GeneralUtilities.ensure_directory_does_not_exist(path)
641
+
642
+ @staticmethod
643
+ @check_arguments
644
+ def format_xml_file(filepath: str) -> None:
645
+ GeneralUtilities.format_xml_file_with_encoding(filepath, "utf-8")
646
+
647
+ @staticmethod
648
+ @check_arguments
649
+ def format_xml_file_with_encoding(filepath: str, encoding: str) -> None:
650
+ with codecs.open(filepath, 'r', encoding=encoding) as file:
651
+ text = file.read()
652
+ text = parse(text).toprettyxml()
653
+ with codecs.open(filepath, 'w', encoding=encoding) as file:
654
+ file.write(text)
655
+
656
+ @staticmethod
657
+ @check_arguments
658
+ def get_clusters_and_sectors_of_disk(diskpath: str) -> None:
659
+ GeneralUtilities.assert_condition(GeneralUtilities.current_system_is_windows(), "get_clusters_and_sectors_of_disk(diskpath) is only available on windows.")
660
+ sectorsPerCluster = ctypes.c_ulonglong(0)
661
+ bytesPerSector = ctypes.c_ulonglong(0)
662
+ rootPathName = ctypes.c_wchar_p(diskpath)
663
+ ctypes.windll.kernel32.GetDiskFreeSpaceW(rootPathName, ctypes.pointer(sectorsPerCluster), ctypes.pointer(bytesPerSector), None, None)
664
+ return (sectorsPerCluster.value, bytesPerSector.value)
665
+
666
+ @staticmethod
667
+ @check_arguments
668
+ def ensure_path_is_not_quoted(path: str) -> str:
669
+ if (path.startswith("\"") and path.endswith("\"")) or (path.startswith("'") and path.endswith("'")):
670
+ path = path[1:]
671
+ path = path[:-1]
672
+ return path
673
+ else:
674
+ return path
675
+
676
+ @staticmethod
677
+ @check_arguments
678
+ def get_missing_files(folderA: str, folderB: str) -> list:
679
+ folderA_length = len(folderA)
680
+ result = []
681
+ for fileA in GeneralUtilities.absolute_file_paths(folderA):
682
+ file = fileA[folderA_length:]
683
+ fileB = folderB + file
684
+ if not os.path.isfile(fileB):
685
+ result.append(fileB)
686
+ return result
687
+
688
+ @staticmethod
689
+ @check_arguments
690
+ def to_pascal_case(s: str) -> str:
691
+ return ''.join(current.lower() if prev.isalnum() else current.upper() for prev, current in zip(' ' + s, s) if current.isalnum())
692
+
693
+ @staticmethod
694
+ @check_arguments
695
+ def find_between(s: str, start: str, end: str) -> str:
696
+ return s.split(start)[1].split(end)[0]
697
+
698
+ @staticmethod
699
+ @check_arguments
700
+ def write_lines_to_file(file: str, lines: list, encoding="utf-8") -> None:
701
+ lines = [GeneralUtilities.strip_new_line_character(line) for line in lines]
702
+ content = "\n".join(lines)
703
+ GeneralUtilities.write_text_to_file(file, content, encoding)
704
+
705
+ @staticmethod
706
+ @check_arguments
707
+ def write_text_to_file(file: str, content: str, encoding="utf-8") -> None:
708
+ GeneralUtilities.write_binary_to_file(file, bytes(bytearray(content, encoding)))
709
+
710
+ @staticmethod
711
+ @check_arguments
712
+ def write_binary_to_file(file: str, content: bytes) -> None:
713
+ with open(file, "wb") as file_object:
714
+ file_object.write(content)
715
+
716
+ @staticmethod
717
+ def is_binary_file(path: str):
718
+ content = GeneralUtilities.read_binary_from_file(path)
719
+ 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']
720
+ for binary_content_indicator in binary_content_indicators:
721
+ if binary_content_indicator in content:
722
+ return True
723
+ return False
724
+
725
+ @staticmethod
726
+ @check_arguments
727
+ def read_lines_from_file(file: str, encoding="utf-8") -> list[str]:
728
+ content = GeneralUtilities.read_text_from_file(file, encoding)
729
+ if len(content) == 0:
730
+ return []
731
+ else:
732
+ return [GeneralUtilities.strip_new_line_character(line) for line in content.split('\n')]
733
+
734
+ @staticmethod
735
+ @check_arguments
736
+ def read_nonempty_lines_from_file(file: str, encoding="utf-8") -> list[str]:
737
+ return [line for line in GeneralUtilities.read_lines_from_file(file, encoding) if GeneralUtilities.string_has_content(line)]
738
+
739
+ @staticmethod
740
+ @check_arguments
741
+ def read_text_from_file(file: str, encoding="utf-8") -> str:
742
+ GeneralUtilities.assert_file_exists(file)
743
+ return GeneralUtilities.bytes_to_string(GeneralUtilities.read_binary_from_file(file), encoding)
744
+
745
+ @staticmethod
746
+ @check_arguments
747
+ def read_binary_from_file(file: str) -> bytes:
748
+ with open(file, "rb") as file_object:
749
+ return file_object.read()
750
+
751
+ @staticmethod
752
+ @check_arguments
753
+ def timedelta_to_simple_string(delta: timedelta) -> str:
754
+ return (datetime(1970, 1, 1, 0, 0, 0) + delta).strftime('%H:%M:%S')
755
+
756
+ @staticmethod
757
+ @check_arguments
758
+ def resolve_relative_path_from_current_working_directory(path: str) -> str:
759
+ return GeneralUtilities.resolve_relative_path(path, os.getcwd())
760
+
761
+ @staticmethod
762
+ @check_arguments
763
+ def resolve_relative_path(path: str, base_path: str):
764
+ if (os.path.isabs(path)):
765
+ return path
766
+ else:
767
+ return str(Path(os.path.join(base_path, path)).resolve())
768
+
769
+ @staticmethod
770
+ @check_arguments
771
+ def get_metadata_for_file_for_clone_folder_structure(file: str) -> str:
772
+ size = os.path.getsize(file)
773
+ last_modified_timestamp = os.path.getmtime(file)
774
+ hash_value = GeneralUtilities.get_sha256_of_file(file)
775
+ last_access_timestamp = os.path.getatime(file)
776
+ return f'{{"size":"{size}","sha256":"{hash_value}","mtime":"{last_modified_timestamp}","atime":"{last_access_timestamp}"}}'
777
+
778
+ @staticmethod
779
+ @check_arguments
780
+ def clone_folder_structure(source: str, target: str, copy_only_metadata: bool):
781
+ source = GeneralUtilities.resolve_relative_path(source, os.getcwd())
782
+ target = GeneralUtilities.resolve_relative_path(target, os.getcwd())
783
+ length_of_source = len(source)
784
+ for source_file in GeneralUtilities.absolute_file_paths(source):
785
+ target_file = target+source_file[length_of_source:]
786
+ GeneralUtilities.ensure_directory_exists(os.path.dirname(target_file))
787
+ if copy_only_metadata:
788
+ with open(target_file, 'w', encoding='utf8') as f:
789
+ f.write(GeneralUtilities.get_metadata_for_file_for_clone_folder_structure(source_file))
790
+ else:
791
+ shutil.copyfile(source_file, target_file)
792
+
793
+ @staticmethod
794
+ @check_arguments
795
+ def current_user_has_elevated_privileges() -> bool:
796
+ try:
797
+ return os.getuid() == 0
798
+ except AttributeError:
799
+ return ctypes.windll.shell32.IsUserAnAdmin() == 1
800
+
801
+ @staticmethod
802
+ @check_arguments
803
+ def ensure_elevated_privileges() -> None:
804
+ if (not GeneralUtilities.current_user_has_elevated_privileges()):
805
+ raise ValueError("Not enough privileges.")
806
+
807
+ @staticmethod
808
+ @check_arguments
809
+ def rename_names_of_all_files_and_folders(folder: str, replace_from: str, replace_to: str, replace_only_full_match=False):
810
+ for file in GeneralUtilities.get_direct_files_of_folder(folder):
811
+ GeneralUtilities.replace_in_filename(file, replace_from, replace_to, replace_only_full_match)
812
+ for sub_folder in GeneralUtilities.get_direct_folders_of_folder(folder):
813
+ GeneralUtilities.rename_names_of_all_files_and_folders(sub_folder, replace_from, replace_to, replace_only_full_match)
814
+ GeneralUtilities.replace_in_foldername(folder, replace_from, replace_to, replace_only_full_match)
815
+
816
+ @staticmethod
817
+ @check_arguments
818
+ def get_direct_files_of_folder(folder: str) -> list[str]:
819
+ result = [os.path.join(folder, f) for f in listdir(folder) if isfile(join(folder, f))]
820
+ result = sorted(result, key=str.casefold)
821
+ return result
822
+
823
+ @staticmethod
824
+ @check_arguments
825
+ def get_direct_folders_of_folder(folder: str) -> list[str]:
826
+ result = [os.path.join(folder, f) for f in listdir(folder) if isdir(join(folder, f))]
827
+ result = sorted(result, key=str.casefold)
828
+ return result
829
+
830
+ @staticmethod
831
+ @check_arguments
832
+ def get_all_files_of_folder(folder: str) -> list[str]:
833
+ result = list()
834
+ result.extend(GeneralUtilities.get_direct_files_of_folder(folder))
835
+ for subfolder in GeneralUtilities.get_direct_folders_of_folder(folder):
836
+ result.extend(GeneralUtilities.get_all_files_of_folder(subfolder))
837
+ result = sorted(result, key=str.casefold)
838
+ return result
839
+
840
+ @staticmethod
841
+ @check_arguments
842
+ def get_all_folders_of_folder(folder: str) -> list[str]:
843
+ result = list()
844
+ subfolders = GeneralUtilities.get_direct_folders_of_folder(folder)
845
+ result.extend(subfolders)
846
+ for subfolder in subfolders:
847
+ result.extend(GeneralUtilities.get_all_folders_of_folder(subfolder))
848
+ result = sorted(result, key=str.casefold)
849
+ return result
850
+
851
+ @staticmethod
852
+ @check_arguments
853
+ def get_all_objects_of_folder(folder: str) -> list[str]:
854
+ return sorted(GeneralUtilities.get_all_files_of_folder(folder) + GeneralUtilities.get_all_folders_of_folder(folder), key=str.casefold)
855
+
856
+ @staticmethod
857
+ @check_arguments
858
+ def replace_in_filename(file: str, replace_from: str, replace_to: str, replace_only_full_match=False):
859
+ filename = Path(file).name
860
+ if (GeneralUtilities.__should_get_replaced(filename, replace_from, replace_only_full_match)):
861
+ folder_of_file = os.path.dirname(file)
862
+ os.rename(file, os.path.join(folder_of_file, filename.replace(replace_from, replace_to)))
863
+
864
+ @staticmethod
865
+ @check_arguments
866
+ def replace_in_foldername(folder: str, replace_from: str, replace_to: str, replace_only_full_match=False):
867
+ foldername = Path(folder).name
868
+ if (GeneralUtilities.__should_get_replaced(foldername, replace_from, replace_only_full_match)):
869
+ folder_of_folder = os.path.dirname(folder)
870
+ os.rename(folder, os.path.join(folder_of_folder, foldername.replace(replace_from, replace_to)))
871
+
872
+ @staticmethod
873
+ @check_arguments
874
+ def __should_get_replaced(input_text, search_text, replace_only_full_match) -> bool:
875
+ if replace_only_full_match:
876
+ return input_text == search_text
877
+ else:
878
+ return search_text in input_text
879
+
880
+ @staticmethod
881
+ @check_arguments
882
+ def str_none_safe(variable) -> str:
883
+ if variable is None:
884
+ return ''
885
+ else:
886
+ return str(variable)
887
+
888
+ @staticmethod
889
+ @check_arguments
890
+ def arguments_to_array(arguments_as_string: str) -> list[str]:
891
+ if arguments_as_string is None:
892
+ return []
893
+ if GeneralUtilities.string_has_content(arguments_as_string):
894
+ return arguments_as_string.split(" ") # TODO this function should get improved to allow whitespaces in quote-substrings
895
+ else:
896
+ return []
897
+
898
+ @staticmethod
899
+ @check_arguments
900
+ def arguments_to_array_for_log(arguments_as_string: str) -> list[str]:
901
+ if arguments_as_string is None:
902
+ return None
903
+ return GeneralUtilities.arguments_to_array(arguments_as_string)
904
+
905
+ @staticmethod
906
+ @check_arguments
907
+ def get_sha256_of_file(file: str) -> str:
908
+ sha256 = hashlib.sha256()
909
+ with open(file, "rb") as fileObject:
910
+ for chunk in iter(lambda: fileObject.read(4096), b""):
911
+ sha256.update(chunk)
912
+ return sha256.hexdigest()
913
+
914
+ @staticmethod
915
+ @check_arguments
916
+ def remove_duplicates(input_list) -> list:
917
+ result = []
918
+ for item in input_list:
919
+ if not item in result:
920
+ result.append(item)
921
+ return result
922
+
923
+ @staticmethod
924
+ @check_arguments
925
+ def print_stacktrace() -> None:
926
+ for line in traceback.format_stack():
927
+ GeneralUtilities.write_message_to_stdout(line.strip())
928
+
929
+ @staticmethod
930
+ @check_arguments
931
+ def string_to_boolean(value: str) -> bool:
932
+ value = value.strip().lower()
933
+ if value in ('yes', 'y', 'true', 't', '1'):
934
+ return True
935
+ elif value in ('no', 'n', 'false', 'f', '0'):
936
+ return False
937
+ else:
938
+ raise ValueError(f"Can not convert '{value}' to a boolean value")
939
+
940
+ @staticmethod
941
+ @check_arguments
942
+ def file_is_empty(file: str) -> bool:
943
+ return os.stat(file).st_size == 0
944
+
945
+ @staticmethod
946
+ @check_arguments
947
+ def folder_is_empty(folder: str) -> bool:
948
+ return len(GeneralUtilities.get_direct_files_of_folder(folder)) == 0 and len(GeneralUtilities.get_direct_folders_of_folder(folder)) == 0
949
+
950
+ @staticmethod
951
+ @check_arguments
952
+ def get_time_based_logfile_by_folder(folder: str, name: str = "Log") -> str:
953
+ return os.path.join(GeneralUtilities.resolve_relative_path_from_current_working_directory(folder), f"{GeneralUtilities.get_time_based_logfilename(name)}.log")
954
+
955
+ @staticmethod
956
+ @check_arguments
957
+ def get_now() -> datetime:
958
+ return datetime.now().astimezone()
959
+
960
+ @staticmethod
961
+ @check_arguments
962
+ def get_time_based_logfilename(name: str = "Log") -> str:
963
+ d = GeneralUtilities.get_now()
964
+ return f"{name}_{GeneralUtilities.datetime_to_string_for_logfile_name(d)}"
965
+
966
+ @staticmethod
967
+ @check_arguments
968
+ def bytes_to_string(payload: bytes, encoding: str = 'utf-8') -> str:
969
+ return payload.decode(encoding, errors="ignore")
970
+
971
+ @staticmethod
972
+ @check_arguments
973
+ def string_to_bytes(payload: str, encoding: str = 'utf-8') -> bytes:
974
+ return payload.encode(encoding, errors="ignore")
975
+
976
+ @staticmethod
977
+ @check_arguments
978
+ def contains_line(lines, regex: str) -> bool:
979
+ for line in lines:
980
+ if (re.match(regex, line)):
981
+ return True
982
+ return False
983
+
984
+ @staticmethod
985
+ @check_arguments
986
+ 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]]:
987
+ lines = GeneralUtilities.read_lines_from_file(file, encoding)
988
+
989
+ if ignore_first_line:
990
+ lines = lines[1:]
991
+ result = list()
992
+ line: str
993
+ for line_loopvariable in lines:
994
+ use_line = True
995
+ line = line_loopvariable
996
+
997
+ if trim_values:
998
+ line = line.strip()
999
+ if ignore_empty_lines:
1000
+ if not GeneralUtilities.string_has_content(line):
1001
+ use_line = False
1002
+
1003
+ if treat_number_sign_at_begin_of_line_as_comment:
1004
+ if line.startswith("#"):
1005
+ use_line = False
1006
+
1007
+ if use_line:
1008
+ if separator_character in line:
1009
+ raw_values_of_line = GeneralUtilities.to_list(line, separator_character)
1010
+ else:
1011
+ raw_values_of_line = [line]
1012
+ if trim_values:
1013
+ raw_values_of_line = [value.strip() for value in raw_values_of_line]
1014
+ values_of_line = []
1015
+ for raw_value_of_line in raw_values_of_line:
1016
+ value_of_line = raw_value_of_line
1017
+ if values_are_surrounded_by_quotes:
1018
+ value_of_line = value_of_line[1:]
1019
+ value_of_line = value_of_line[:-1]
1020
+ value_of_line = value_of_line.replace('""', '"')
1021
+ values_of_line.append(value_of_line)
1022
+ result.extend([values_of_line])
1023
+ return result
1024
+
1025
+ @staticmethod
1026
+ @check_arguments
1027
+ def epew_is_available() -> bool:
1028
+ return GeneralUtilities.tool_is_available("epew")
1029
+
1030
+ @staticmethod
1031
+ @check_arguments
1032
+ def tool_is_available(toolname: str) -> bool:
1033
+ try:
1034
+ return shutil.which(toolname) is not None
1035
+ except:
1036
+ return False
1037
+
1038
+ @staticmethod
1039
+ @check_arguments
1040
+ @deprecated
1041
+ def absolute_file_paths(directory: str) -> list[str]:
1042
+ return GeneralUtilities.get_all_files_of_folder(directory)
1043
+
1044
+ @staticmethod
1045
+ @check_arguments
1046
+ def to_list(list_as_string: str, separator: str = ",") -> list[str]:
1047
+ result = list()
1048
+ if list_as_string is not None:
1049
+ list_as_string = list_as_string.strip()
1050
+ if list_as_string == GeneralUtilities.empty_string:
1051
+ pass
1052
+ elif separator in list_as_string:
1053
+ for item in list_as_string.split(separator):
1054
+ result.append(item.strip())
1055
+ else:
1056
+ result.append(list_as_string)
1057
+ return result
1058
+
1059
+ @staticmethod
1060
+ @check_arguments
1061
+ def get_next_square_number(number: int) -> int:
1062
+ GeneralUtilities.assert_condition(number >= 0, "get_next_square_number is only applicable for nonnegative numbers")
1063
+ if number == 0:
1064
+ return 1
1065
+ root = 0
1066
+ square = 0
1067
+ while square < number:
1068
+ root = root+1
1069
+ square = root*root
1070
+ return root*root
1071
+
1072
+ @staticmethod
1073
+ @check_arguments
1074
+ def generate_password(length: int = 16, alphabet: str = None) -> None:
1075
+ if alphabet is None:
1076
+ alphabet = strin.ascii_letters + strin.digits+"_"
1077
+ return ''.join(secrets.choice(alphabet) for i in range(length))
1078
+
1079
+ @staticmethod
1080
+ @check_arguments
1081
+ def assert_condition(condition: bool, information: str = None) -> None:
1082
+ """Throws an exception if the condition is false."""
1083
+ if (not condition):
1084
+ if information is None:
1085
+ information = "Internal assertion error."
1086
+ raise ValueError("Condition failed. "+information)
1087
+
1088
+ @staticmethod
1089
+ def current_system_is_windows():
1090
+ return platform.system() == 'Windows'
1091
+
1092
+ @staticmethod
1093
+ def current_system_is_linux():
1094
+ return platform.system() == 'Linux'
1095
+
1096
+ @staticmethod
1097
+ @check_arguments
1098
+ def get_line():
1099
+ return "--------------------------"
1100
+
1101
+ @staticmethod
1102
+ def get_longline():
1103
+ return GeneralUtilities.get_line() + GeneralUtilities.get_line()
1104
+
1105
+ @staticmethod
1106
+ @check_arguments
1107
+ def get_icon_check_empty(positive: bool) -> str:
1108
+ if positive:
1109
+ return "✅"
1110
+ else:
1111
+ return GeneralUtilities.empty_string
1112
+
1113
+ @staticmethod
1114
+ @check_arguments
1115
+ def get_icon_check_cross(positive: bool) -> str:
1116
+ if positive:
1117
+ return "✅"
1118
+ else:
1119
+ return "❌"
1120
+
1121
+ @staticmethod
1122
+ @check_arguments
1123
+ def get_certificate_expiry_date(certificate_file: str) -> datetime:
1124
+ with open(certificate_file, encoding="utf-8") as certificate_file_content:
1125
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, certificate_file_content.read())
1126
+ date_as_bytes = cert.get_notAfter()
1127
+ date_as_string = date_as_bytes.decode("utf-8")
1128
+ result = datetime.strptime(date_as_string, '%Y%m%d%H%M%SZ')
1129
+ return result
1130
+
1131
+ @staticmethod
1132
+ @check_arguments
1133
+ def certificate_is_expired(certificate_file: str) -> bool:
1134
+ return GeneralUtilities.get_certificate_expiry_date(certificate_file) < GeneralUtilities.get_now()
1135
+
1136
+ @staticmethod
1137
+ @check_arguments
1138
+ def internet_connection_is_available() -> bool:
1139
+ # TODO add more hosts to check to return true if at least one is available
1140
+ try:
1141
+ with urllib.request.urlopen("https://www.google.com") as url_result:
1142
+ return (url_result.code // 100) == 2
1143
+ except:
1144
+ pass
1145
+ return False
1146
+
1147
+ @staticmethod
1148
+ @check_arguments
1149
+ def replace_variable_in_string(input_string: str, variable_name: str, variable_value: str) -> None:
1150
+ GeneralUtilities.assert_condition(not "__" in variable_name, f"'{variable_name}' is an invalid variable name because it contains '__' which is treated as control-sequence.")
1151
+ return input_string.replace(f"__[{variable_name}]__", variable_value)
1152
+
1153
+ @staticmethod
1154
+ @check_arguments
1155
+ 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.
1156
+ GeneralUtilities.write_message_to_stdout(prompt)
1157
+ result: str = input()
1158
+ if print_result:
1159
+ GeneralUtilities.write_message_to_stdout(f"Result: {result}")
1160
+ return result
1161
+
1162
+ @staticmethod
1163
+ @check_arguments
1164
+ def run_program_simple(program: str, arguments: list[str], cwd: str = None) -> tuple[int, str, str]:
1165
+ if cwd is None:
1166
+ cwd = os.getcwd()
1167
+ cmd = [program]+arguments
1168
+ with subprocess.Popen(cmd, cwd=cwd, stderr=subprocess.PIPE, stdout=subprocess.PIPE) as process:
1169
+ stdout, stderr = process.communicate()
1170
+ exit_code = process.wait()
1171
+ return (exit_code, stdout, stderr)
1172
+
1173
+ @staticmethod
1174
+ @check_arguments
1175
+ def assert_file_exists(file: str,message=None) -> None:
1176
+ if message is None:
1177
+ message=f"File '{file}' does not exist."
1178
+ GeneralUtilities.assert_condition(os.path.isfile(file), message)
1179
+
1180
+ @staticmethod
1181
+ @check_arguments
1182
+ def assert_file_does_not_exist(file: str,message=None) -> None:
1183
+ if message is None:
1184
+ message=f"File '{file}' exists."
1185
+ GeneralUtilities.assert_condition(not os.path.isfile(file), message)
1186
+
1187
+ @staticmethod
1188
+ @check_arguments
1189
+ def assert_folder_exists(folder: str,message=None) -> None:
1190
+ if message is None:
1191
+ message=f"Folder '{folder}' does not exist."
1192
+ GeneralUtilities.assert_condition(os.path.isdir(folder),message )
1193
+
1194
+ @staticmethod
1195
+ @check_arguments
1196
+ def assert_folder_does_not_exist(folder: str,message=None) -> None:
1197
+ if message is None:
1198
+ message= f"Folder '{folder}' exists."
1199
+ GeneralUtilities.assert_condition(not os.path.isdir(folder), f"Folder '{folder}' exists.")
1200
+
1201
+ @staticmethod
1202
+ @check_arguments
1203
+ def assert_not_null(obj,message:str=None) -> str:
1204
+ if message is None:
1205
+ message="Variable is not set"
1206
+ GeneralUtilities.assert_condition(obj is not None, message)
1207
+
1208
+ @staticmethod
1209
+ @check_arguments
1210
+ def retry_action(action, amount_of_attempts: int, action_name: str = None) -> None:
1211
+ amount_of_fails = 0
1212
+ enabled = True
1213
+ while enabled:
1214
+ try:
1215
+ result = action()
1216
+ return result
1217
+ except Exception:
1218
+ time.sleep(1.1)
1219
+ amount_of_fails = amount_of_fails+1
1220
+ GeneralUtilities.assert_condition(not (amount_of_attempts < amount_of_fails))
1221
+ message = f"Action failed {amount_of_attempts} times."
1222
+ if action_name is not None:
1223
+ message = f"{message} Name of action: {action_name}"
1224
+ GeneralUtilities.write_message_to_stderr(message)
1225
+ raise
1226
+ return None
1227
+
1228
+ @staticmethod
1229
+ @check_arguments
1230
+ def int_to_string(number: int, leading_zeroplaces: int, trailing_zeroplaces: int) -> str:
1231
+ return GeneralUtilities.float_to_string(float(number), leading_zeroplaces, trailing_zeroplaces)
1232
+
1233
+ @staticmethod
1234
+ @check_arguments
1235
+ def float_to_string(number: float, leading_zeroplaces: int, trailing_zeroplaces: int) -> str:
1236
+ plain_str = str(number)
1237
+ GeneralUtilities.assert_condition("." in plain_str)
1238
+ splitted: list[str] = plain_str.split(".")
1239
+ return splitted[0].zfill(leading_zeroplaces)+"."+splitted[1].ljust(trailing_zeroplaces, '0')
1240
+
1241
+ @staticmethod
1242
+ @check_arguments
1243
+ def process_is_running_by_name(process_name: str) -> bool:
1244
+ processes: list[psutil.Process] = list(psutil.process_iter())
1245
+ for p in processes:
1246
+ if p.name() == process_name:
1247
+ return True
1248
+ return False
1249
+
1250
+ @staticmethod
1251
+ @check_arguments
1252
+ def process_is_running_by_id(process_id: int) -> bool:
1253
+ processes: list[psutil.Process] = list(psutil.process_iter())
1254
+ for p in processes:
1255
+ if p.pid == process_id:
1256
+ return True
1257
+ return False
1258
+
1259
+ @staticmethod
1260
+ @check_arguments
1261
+ def kill_process(process_id: int, include_child_processes: bool) -> bool:
1262
+ if GeneralUtilities. process_is_running_by_id(process_id):
1263
+ GeneralUtilities.write_message_to_stdout(f"Process with id {process_id} is running. Terminating it...")
1264
+ process = psutil.Process(process_id)
1265
+ if include_child_processes:
1266
+ for child in process.children(recursive=True):
1267
+ if GeneralUtilities.process_is_running_by_id(child.pid):
1268
+ child.kill()
1269
+ if GeneralUtilities.process_is_running_by_id(process_id):
1270
+ process.kill()
1271
+ else:
1272
+ GeneralUtilities.write_message_to_stdout(f"Process with id {process_id} is not running anymore.")