amd-node-scraper 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. amd_node_scraper-0.0.1.dist-info/LICENSE +21 -0
  2. amd_node_scraper-0.0.1.dist-info/METADATA +424 -0
  3. amd_node_scraper-0.0.1.dist-info/RECORD +197 -0
  4. amd_node_scraper-0.0.1.dist-info/WHEEL +5 -0
  5. amd_node_scraper-0.0.1.dist-info/entry_points.txt +2 -0
  6. amd_node_scraper-0.0.1.dist-info/top_level.txt +1 -0
  7. nodescraper/__init__.py +32 -0
  8. nodescraper/base/__init__.py +34 -0
  9. nodescraper/base/inbandcollectortask.py +118 -0
  10. nodescraper/base/inbanddataplugin.py +39 -0
  11. nodescraper/base/regexanalyzer.py +120 -0
  12. nodescraper/cli/__init__.py +29 -0
  13. nodescraper/cli/cli.py +511 -0
  14. nodescraper/cli/constants.py +27 -0
  15. nodescraper/cli/dynamicparserbuilder.py +171 -0
  16. nodescraper/cli/helper.py +517 -0
  17. nodescraper/cli/inputargtypes.py +129 -0
  18. nodescraper/configbuilder.py +123 -0
  19. nodescraper/configregistry.py +66 -0
  20. nodescraper/configs/node_status.json +19 -0
  21. nodescraper/connection/__init__.py +25 -0
  22. nodescraper/connection/inband/__init__.py +46 -0
  23. nodescraper/connection/inband/inband.py +171 -0
  24. nodescraper/connection/inband/inbandlocal.py +93 -0
  25. nodescraper/connection/inband/inbandmanager.py +151 -0
  26. nodescraper/connection/inband/inbandremote.py +173 -0
  27. nodescraper/connection/inband/sshparams.py +43 -0
  28. nodescraper/constants.py +26 -0
  29. nodescraper/enums/__init__.py +40 -0
  30. nodescraper/enums/eventcategory.py +89 -0
  31. nodescraper/enums/eventpriority.py +42 -0
  32. nodescraper/enums/executionstatus.py +44 -0
  33. nodescraper/enums/osfamily.py +34 -0
  34. nodescraper/enums/systeminteraction.py +41 -0
  35. nodescraper/enums/systemlocation.py +33 -0
  36. nodescraper/generictypes.py +36 -0
  37. nodescraper/interfaces/__init__.py +44 -0
  38. nodescraper/interfaces/connectionmanager.py +143 -0
  39. nodescraper/interfaces/dataanalyzertask.py +138 -0
  40. nodescraper/interfaces/datacollectortask.py +185 -0
  41. nodescraper/interfaces/dataplugin.py +356 -0
  42. nodescraper/interfaces/plugin.py +127 -0
  43. nodescraper/interfaces/resultcollator.py +56 -0
  44. nodescraper/interfaces/task.py +164 -0
  45. nodescraper/interfaces/taskresulthook.py +39 -0
  46. nodescraper/models/__init__.py +48 -0
  47. nodescraper/models/analyzerargs.py +93 -0
  48. nodescraper/models/collectorargs.py +30 -0
  49. nodescraper/models/connectionconfig.py +34 -0
  50. nodescraper/models/datamodel.py +171 -0
  51. nodescraper/models/datapluginresult.py +39 -0
  52. nodescraper/models/event.py +158 -0
  53. nodescraper/models/pluginconfig.py +38 -0
  54. nodescraper/models/pluginresult.py +39 -0
  55. nodescraper/models/systeminfo.py +44 -0
  56. nodescraper/models/taskresult.py +185 -0
  57. nodescraper/models/timerangeargs.py +38 -0
  58. nodescraper/pluginexecutor.py +274 -0
  59. nodescraper/pluginregistry.py +152 -0
  60. nodescraper/plugins/__init__.py +25 -0
  61. nodescraper/plugins/inband/__init__.py +25 -0
  62. nodescraper/plugins/inband/amdsmi/__init__.py +28 -0
  63. nodescraper/plugins/inband/amdsmi/amdsmi_analyzer.py +821 -0
  64. nodescraper/plugins/inband/amdsmi/amdsmi_collector.py +1313 -0
  65. nodescraper/plugins/inband/amdsmi/amdsmi_plugin.py +43 -0
  66. nodescraper/plugins/inband/amdsmi/amdsmidata.py +1002 -0
  67. nodescraper/plugins/inband/amdsmi/analyzer_args.py +50 -0
  68. nodescraper/plugins/inband/amdsmi/cper.py +65 -0
  69. nodescraper/plugins/inband/bios/__init__.py +29 -0
  70. nodescraper/plugins/inband/bios/analyzer_args.py +64 -0
  71. nodescraper/plugins/inband/bios/bios_analyzer.py +93 -0
  72. nodescraper/plugins/inband/bios/bios_collector.py +93 -0
  73. nodescraper/plugins/inband/bios/bios_plugin.py +43 -0
  74. nodescraper/plugins/inband/bios/biosdata.py +30 -0
  75. nodescraper/plugins/inband/cmdline/__init__.py +25 -0
  76. nodescraper/plugins/inband/cmdline/analyzer_args.py +80 -0
  77. nodescraper/plugins/inband/cmdline/cmdline_analyzer.py +113 -0
  78. nodescraper/plugins/inband/cmdline/cmdline_collector.py +77 -0
  79. nodescraper/plugins/inband/cmdline/cmdline_plugin.py +43 -0
  80. nodescraper/plugins/inband/cmdline/cmdlinedata.py +30 -0
  81. nodescraper/plugins/inband/device_enumeration/__init__.py +29 -0
  82. nodescraper/plugins/inband/device_enumeration/analyzer_args.py +73 -0
  83. nodescraper/plugins/inband/device_enumeration/device_enumeration_analyzer.py +81 -0
  84. nodescraper/plugins/inband/device_enumeration/device_enumeration_collector.py +176 -0
  85. nodescraper/plugins/inband/device_enumeration/device_enumeration_plugin.py +45 -0
  86. nodescraper/plugins/inband/device_enumeration/deviceenumdata.py +36 -0
  87. nodescraper/plugins/inband/dimm/__init__.py +25 -0
  88. nodescraper/plugins/inband/dimm/collector_args.py +31 -0
  89. nodescraper/plugins/inband/dimm/dimm_collector.py +151 -0
  90. nodescraper/plugins/inband/dimm/dimm_plugin.py +40 -0
  91. nodescraper/plugins/inband/dimm/dimmdata.py +30 -0
  92. nodescraper/plugins/inband/dkms/__init__.py +25 -0
  93. nodescraper/plugins/inband/dkms/analyzer_args.py +85 -0
  94. nodescraper/plugins/inband/dkms/dkms_analyzer.py +106 -0
  95. nodescraper/plugins/inband/dkms/dkms_collector.py +76 -0
  96. nodescraper/plugins/inband/dkms/dkms_plugin.py +43 -0
  97. nodescraper/plugins/inband/dkms/dkmsdata.py +33 -0
  98. nodescraper/plugins/inband/dmesg/__init__.py +28 -0
  99. nodescraper/plugins/inband/dmesg/analyzer_args.py +33 -0
  100. nodescraper/plugins/inband/dmesg/collector_args.py +39 -0
  101. nodescraper/plugins/inband/dmesg/dmesg_analyzer.py +503 -0
  102. nodescraper/plugins/inband/dmesg/dmesg_collector.py +164 -0
  103. nodescraper/plugins/inband/dmesg/dmesg_plugin.py +44 -0
  104. nodescraper/plugins/inband/dmesg/dmesgdata.py +116 -0
  105. nodescraper/plugins/inband/fabrics/__init__.py +28 -0
  106. nodescraper/plugins/inband/fabrics/fabrics_collector.py +726 -0
  107. nodescraper/plugins/inband/fabrics/fabrics_plugin.py +37 -0
  108. nodescraper/plugins/inband/fabrics/fabricsdata.py +140 -0
  109. nodescraper/plugins/inband/journal/__init__.py +28 -0
  110. nodescraper/plugins/inband/journal/collector_args.py +33 -0
  111. nodescraper/plugins/inband/journal/journal_collector.py +107 -0
  112. nodescraper/plugins/inband/journal/journal_plugin.py +40 -0
  113. nodescraper/plugins/inband/journal/journaldata.py +44 -0
  114. nodescraper/plugins/inband/kernel/__init__.py +25 -0
  115. nodescraper/plugins/inband/kernel/analyzer_args.py +64 -0
  116. nodescraper/plugins/inband/kernel/kernel_analyzer.py +91 -0
  117. nodescraper/plugins/inband/kernel/kernel_collector.py +129 -0
  118. nodescraper/plugins/inband/kernel/kernel_plugin.py +43 -0
  119. nodescraper/plugins/inband/kernel/kerneldata.py +32 -0
  120. nodescraper/plugins/inband/kernel_module/__init__.py +25 -0
  121. nodescraper/plugins/inband/kernel_module/analyzer_args.py +59 -0
  122. nodescraper/plugins/inband/kernel_module/kernel_module_analyzer.py +211 -0
  123. nodescraper/plugins/inband/kernel_module/kernel_module_collector.py +264 -0
  124. nodescraper/plugins/inband/kernel_module/kernel_module_data.py +60 -0
  125. nodescraper/plugins/inband/kernel_module/kernel_module_plugin.py +43 -0
  126. nodescraper/plugins/inband/memory/__init__.py +25 -0
  127. nodescraper/plugins/inband/memory/analyzer_args.py +45 -0
  128. nodescraper/plugins/inband/memory/memory_analyzer.py +98 -0
  129. nodescraper/plugins/inband/memory/memory_collector.py +330 -0
  130. nodescraper/plugins/inband/memory/memory_plugin.py +43 -0
  131. nodescraper/plugins/inband/memory/memorydata.py +90 -0
  132. nodescraper/plugins/inband/network/__init__.py +28 -0
  133. nodescraper/plugins/inband/network/network_collector.py +1828 -0
  134. nodescraper/plugins/inband/network/network_plugin.py +37 -0
  135. nodescraper/plugins/inband/network/networkdata.py +319 -0
  136. nodescraper/plugins/inband/nvme/__init__.py +28 -0
  137. nodescraper/plugins/inband/nvme/nvme_collector.py +167 -0
  138. nodescraper/plugins/inband/nvme/nvme_plugin.py +37 -0
  139. nodescraper/plugins/inband/nvme/nvmedata.py +45 -0
  140. nodescraper/plugins/inband/os/__init__.py +25 -0
  141. nodescraper/plugins/inband/os/analyzer_args.py +64 -0
  142. nodescraper/plugins/inband/os/os_analyzer.py +73 -0
  143. nodescraper/plugins/inband/os/os_collector.py +131 -0
  144. nodescraper/plugins/inband/os/os_plugin.py +43 -0
  145. nodescraper/plugins/inband/os/osdata.py +31 -0
  146. nodescraper/plugins/inband/package/__init__.py +25 -0
  147. nodescraper/plugins/inband/package/analyzer_args.py +48 -0
  148. nodescraper/plugins/inband/package/package_analyzer.py +253 -0
  149. nodescraper/plugins/inband/package/package_collector.py +273 -0
  150. nodescraper/plugins/inband/package/package_plugin.py +43 -0
  151. nodescraper/plugins/inband/package/packagedata.py +41 -0
  152. nodescraper/plugins/inband/pcie/__init__.py +29 -0
  153. nodescraper/plugins/inband/pcie/analyzer_args.py +63 -0
  154. nodescraper/plugins/inband/pcie/pcie_analyzer.py +1081 -0
  155. nodescraper/plugins/inband/pcie/pcie_collector.py +690 -0
  156. nodescraper/plugins/inband/pcie/pcie_data.py +2017 -0
  157. nodescraper/plugins/inband/pcie/pcie_plugin.py +43 -0
  158. nodescraper/plugins/inband/process/__init__.py +25 -0
  159. nodescraper/plugins/inband/process/analyzer_args.py +45 -0
  160. nodescraper/plugins/inband/process/collector_args.py +31 -0
  161. nodescraper/plugins/inband/process/process_analyzer.py +91 -0
  162. nodescraper/plugins/inband/process/process_collector.py +115 -0
  163. nodescraper/plugins/inband/process/process_plugin.py +46 -0
  164. nodescraper/plugins/inband/process/processdata.py +34 -0
  165. nodescraper/plugins/inband/rocm/__init__.py +25 -0
  166. nodescraper/plugins/inband/rocm/analyzer_args.py +66 -0
  167. nodescraper/plugins/inband/rocm/rocm_analyzer.py +100 -0
  168. nodescraper/plugins/inband/rocm/rocm_collector.py +205 -0
  169. nodescraper/plugins/inband/rocm/rocm_plugin.py +43 -0
  170. nodescraper/plugins/inband/rocm/rocmdata.py +62 -0
  171. nodescraper/plugins/inband/storage/__init__.py +25 -0
  172. nodescraper/plugins/inband/storage/analyzer_args.py +38 -0
  173. nodescraper/plugins/inband/storage/collector_args.py +31 -0
  174. nodescraper/plugins/inband/storage/storage_analyzer.py +152 -0
  175. nodescraper/plugins/inband/storage/storage_collector.py +110 -0
  176. nodescraper/plugins/inband/storage/storage_plugin.py +44 -0
  177. nodescraper/plugins/inband/storage/storagedata.py +70 -0
  178. nodescraper/plugins/inband/sysctl/__init__.py +29 -0
  179. nodescraper/plugins/inband/sysctl/analyzer_args.py +67 -0
  180. nodescraper/plugins/inband/sysctl/sysctl_analyzer.py +81 -0
  181. nodescraper/plugins/inband/sysctl/sysctl_collector.py +101 -0
  182. nodescraper/plugins/inband/sysctl/sysctl_plugin.py +43 -0
  183. nodescraper/plugins/inband/sysctl/sysctldata.py +42 -0
  184. nodescraper/plugins/inband/syslog/__init__.py +28 -0
  185. nodescraper/plugins/inband/syslog/syslog_collector.py +121 -0
  186. nodescraper/plugins/inband/syslog/syslog_plugin.py +37 -0
  187. nodescraper/plugins/inband/syslog/syslogdata.py +46 -0
  188. nodescraper/plugins/inband/uptime/__init__.py +25 -0
  189. nodescraper/plugins/inband/uptime/uptime_collector.py +88 -0
  190. nodescraper/plugins/inband/uptime/uptime_plugin.py +37 -0
  191. nodescraper/plugins/inband/uptime/uptimedata.py +31 -0
  192. nodescraper/resultcollators/__init__.py +25 -0
  193. nodescraper/resultcollators/tablesummary.py +159 -0
  194. nodescraper/taskresulthooks/__init__.py +28 -0
  195. nodescraper/taskresulthooks/filesystemloghook.py +88 -0
  196. nodescraper/typeutils.py +171 -0
  197. nodescraper/utils.py +412 -0
@@ -0,0 +1,171 @@
1
+ ###############################################################################
2
+ #
3
+ # MIT License
4
+ #
5
+ # Copyright (c) 2025 Advanced Micro Devices, Inc.
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to deal
9
+ # in the Software without restriction, including without limitation the rights
10
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ # copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in all
15
+ # copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ # SOFTWARE.
24
+ #
25
+ ###############################################################################
26
+ import inspect
27
+ import types
28
+ from typing import Any, Callable, Optional, Type, Union, get_args, get_origin
29
+
30
+ from pydantic import BaseModel, Field
31
+
32
+
33
+ class TypeClass(BaseModel):
34
+ """Class to hold type class information"""
35
+
36
+ type_class: Any
37
+ inner_type: Optional[Any] = None
38
+
39
+
40
+ class TypeData(BaseModel):
41
+ """Class to hold type data information"""
42
+
43
+ type_classes: list[TypeClass] = Field(default_factory=list)
44
+ required: bool = False
45
+ default: Any = None
46
+
47
+
48
+ class TypeUtils:
49
+
50
+ @classmethod
51
+ def get_generic_map(cls, class_type: Type[Any]) -> dict:
52
+ """Get a map of generic type parameters to their actual types for a class
53
+
54
+ Args:
55
+ class_type (Type[Any]): class to check for generic types
56
+
57
+ Returns:
58
+ dict: map of generic type parameters to their actual types
59
+ """
60
+ if class_type.__orig_bases__ and len(class_type.__orig_bases__) > 0:
61
+ gen_base = class_type.__orig_bases__[0]
62
+ class_org = get_origin(gen_base)
63
+ args = get_args(gen_base)
64
+ generic_map = dict(zip(class_org.__parameters__, args))
65
+ else:
66
+ generic_map = {}
67
+
68
+ return generic_map
69
+
70
+ @classmethod
71
+ def get_func_arg_types(
72
+ cls, target: Callable, class_type: Optional[Type[Any]] = None
73
+ ) -> dict[str, TypeData]:
74
+ """Get argument type details for a function
75
+
76
+ Args:
77
+ target (Callable): function to check types
78
+ class_type (Optional[Type[Any]], optional): class that the function belongs to, if any. Defaults to None.
79
+
80
+ Returns:
81
+ dict[str, TypeData]: map of argument names to TypeData objects containing type information
82
+ """
83
+
84
+ generic_map = {}
85
+
86
+ if class_type:
87
+ generic_map = cls.get_generic_map(class_type)
88
+
89
+ type_map = {}
90
+ skip_args = ["self"]
91
+ for arg, param in inspect.signature(target).parameters.items():
92
+ if arg in skip_args:
93
+ continue
94
+
95
+ type_data = TypeData()
96
+
97
+ type_classes = cls.process_type(param.annotation)
98
+ for type_class in type_classes:
99
+ if type_class.type_class in generic_map:
100
+ type_class.type_class = generic_map[type_class.type_class]
101
+
102
+ type_data.type_classes = type_classes
103
+ if param.default is inspect.Parameter.empty:
104
+ type_data.required = True
105
+ else:
106
+ type_data.default = param.default
107
+
108
+ type_map[arg] = type_data
109
+
110
+ return type_map
111
+
112
+ @classmethod
113
+ def process_type(cls, input_type: type[Any]) -> list[TypeClass]:
114
+ """Process a type to extract its class and any inner types
115
+
116
+ Args:
117
+ input_type (type[Any]): type to process
118
+
119
+ Returns:
120
+ list[TypeClass]: list of TypeClass objects containing type class and inner type information
121
+ """
122
+ origin = get_origin(input_type)
123
+ if origin is None:
124
+ return [TypeClass(type_class=input_type)]
125
+ if origin is Union or getattr(types, "UnionType", None) is origin:
126
+ type_classes = []
127
+ input_types = [arg for arg in input_type.__args__ if arg is not type(None)]
128
+ for type_item in input_types:
129
+ origin = get_origin(type_item)
130
+ if origin is None:
131
+ type_classes.append(TypeClass(type_class=type_item))
132
+ else:
133
+ type_classes.append(
134
+ TypeClass(
135
+ type_class=origin,
136
+ inner_type=next(
137
+ (arg for arg in get_args(type_item) if arg is not type(None)), None
138
+ ),
139
+ )
140
+ )
141
+
142
+ return type_classes
143
+ else:
144
+ return [
145
+ TypeClass(
146
+ type_class=origin,
147
+ inner_type=next(
148
+ (arg for arg in get_args(input_type) if arg is not type(None)), None
149
+ ),
150
+ )
151
+ ]
152
+
153
+ @classmethod
154
+ def get_model_types(cls, model: type[BaseModel]) -> dict[str, TypeData]:
155
+ """Get model attribute type details for a pydantic model
156
+
157
+ Args:
158
+ model (type[BaseModel]): model to check types
159
+
160
+ Returns:
161
+ dict[str, TypeData]: map of type info
162
+ """
163
+ type_map = {}
164
+ for name, field in model.model_fields.items():
165
+ type_map[name] = TypeData(
166
+ type_classes=cls.process_type(field.annotation),
167
+ required=field.is_required(),
168
+ default=field.default,
169
+ )
170
+
171
+ return type_map
nodescraper/utils.py ADDED
@@ -0,0 +1,412 @@
1
+ ###############################################################################
2
+ #
3
+ # MIT License
4
+ #
5
+ # Copyright (c) 2025 Advanced Micro Devices, Inc.
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to deal
9
+ # in the Software without restriction, including without limitation the rights
10
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ # copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in all
15
+ # copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ # SOFTWARE.
24
+ #
25
+ ###############################################################################
26
+ import inspect
27
+ import os
28
+ import re
29
+ import traceback
30
+ from enum import Enum
31
+ from typing import Any, List, Optional, Set, Type, TypeVar, Union, get_args, get_origin
32
+
33
+ T = TypeVar("T")
34
+
35
+
36
+ class AutoNameStrEnum(Enum):
37
+ """For enums where the value is the same as the name of the attribute"""
38
+
39
+ @staticmethod
40
+ def _generate_next_value_(name, start, count, last_values):
41
+ """Name is the attributes name and the return will be its value"""
42
+ return name
43
+
44
+
45
+ def get_exception_traceback(exception: Exception) -> dict:
46
+ """get traceback and exception type from an exception
47
+
48
+ Args:
49
+ exception (Exception): exception
50
+
51
+ Returns:
52
+ dict: exception details dict
53
+ """
54
+ return {
55
+ "exception_type": type(exception).__name__,
56
+ "traceback": traceback.format_tb(exception.__traceback__),
57
+ }
58
+
59
+
60
+ def get_exception_details(exception: Exception) -> dict:
61
+ """get exception as a string and format in dictionary for event
62
+
63
+ Args:
64
+ exception (Exception): exception
65
+
66
+ Returns:
67
+ dict: exception details dict
68
+ """
69
+ return {
70
+ "details": str(exception)[:1000],
71
+ }
72
+
73
+
74
+ def convert_to_bytes(value: str, si=False) -> int:
75
+ """
76
+ Convert human-readable memory sizes (like GB, MB) to bytes.
77
+ Default to use IEC units.
78
+ Factor of powers of 2, not 10. (e.g. 1KB is interpeted as 1KiB=1024 bytes)
79
+ This can be changed with si=True (1KB=1000 bytes)
80
+ """
81
+ value = value.strip().upper()
82
+ unit_names = ["K", "M", "G", "T", "P", "E", "Z", "Y"]
83
+ if si:
84
+ exponent_base = 10
85
+ exponent_power = 3
86
+ else:
87
+ exponent_base = 2
88
+ exponent_power = 10
89
+ # Extract the numeric part and the unit
90
+ pattern = re.compile(r"(\d+\.?\d*)([YZEPTGMK]?)")
91
+ match = pattern.match(value)
92
+ if not match:
93
+ raise ValueError(f"Invalid memory value: {value}")
94
+
95
+ # Handle the numeric value and ensure it's a valid number
96
+ try:
97
+ value = float(match.group(1))
98
+ except ValueError as err:
99
+ raise ValueError(f"Invalid numeric value in: {value}") from err
100
+
101
+ unit = match.group(2)
102
+
103
+ # Convert the value to bytes
104
+ for unit_index, unit_name in enumerate(unit_names):
105
+ if unit == unit_name:
106
+ return int(float(value) * (exponent_base ** ((unit_index + 1) * exponent_power)))
107
+ # If the unit is not found, it is bytes
108
+ return int(value)
109
+
110
+
111
+ def get_unique_filename(directory, filename) -> str:
112
+ """Checks if the file exists in the directory and returns a new filename if it does.
113
+ Parameters
114
+ ----------
115
+ directory : str
116
+ Directory of the file to be saved
117
+ filename : str
118
+ Proposed name of the file to save, unique filename will be generated based on this
119
+ if it already exists, example: "file.txt" -> "file(1).txt" if "file.txt" already exists
120
+ Returns
121
+ -------
122
+ str
123
+ The new unique filename to save
124
+ """
125
+ filepath = os.path.join(directory, filename)
126
+ if not os.path.isfile(filepath):
127
+ return filename
128
+ name, ext = os.path.splitext(filename)
129
+ count = 1
130
+ while True:
131
+ new_name = f"{name}({count}){ext}"
132
+ new_path = os.path.join(directory, new_name)
133
+ if not os.path.exists(new_path):
134
+ return new_name
135
+ count += 1
136
+
137
+
138
+ def pascal_to_snake(input_str: str) -> str:
139
+ """Convert PascalCase to snake_case
140
+
141
+ Args:
142
+ input_str (str): string to convert
143
+
144
+ Returns:
145
+ str: converted string
146
+ """
147
+ if input_str.isupper():
148
+ return input_str.lower()
149
+ return ("_").join(re.split("(?<=.)(?=[A-Z])", input_str)).lower()
150
+
151
+
152
+ def bytes_to_human_readable(input_bytes: int) -> str:
153
+ """converts a bytes int to a human readable sting in KB, MB, or GB
154
+
155
+ Args:
156
+ input_bytes (int): bytes integer
157
+
158
+ Returns:
159
+ str: human readable string
160
+ """
161
+ kb = round(float(input_bytes) / 1000, 2)
162
+
163
+ if kb < 1000:
164
+ return f"{kb}KB"
165
+
166
+ mb = round(kb / 1000, 2)
167
+
168
+ if mb < 1000:
169
+ return f"{mb}MB"
170
+
171
+ gb = round(mb / 1000, 2)
172
+ return f"{gb}GB"
173
+
174
+
175
+ def find_annotation_in_container(
176
+ annotation, target_type
177
+ ) -> Union[tuple[Any, list[Any]], tuple[None, list[Any]]]:
178
+ """Recursively search for a target type in an annotation and return the target type and the containers
179
+ supported container types are generic types, Callable, Tuple, Union, Literal, Final, ClassVar
180
+ and Annotated. If the target type is not found then None is returned.
181
+
182
+ Examples:
183
+ find_annotation_in_container(Union[int, str], int) -> int, [Union[int, str]]
184
+ find_annotation_in_container(Union[int, dict[str, list[MyClass]]], MyClass) -> MyClass, [list,dict,union]
185
+ find_annotation_in_container(Union[int, str], MyClass) -> None, []
186
+
187
+ Parameters
188
+ ----------
189
+ annotation : type
190
+ A type annotation to search for the target type in.
191
+ target_type : type
192
+ The target type to search for.
193
+
194
+ Returns
195
+ -------
196
+ Union[tuple[Any, list[Any]], tuple[None, []]]
197
+ The target type and the containers if found, otherwise None and an empty list.
198
+ """
199
+ containers: list[Any] = []
200
+ origin = get_origin(annotation)
201
+ args = get_args(annotation)
202
+ if len(args) == 0 and issubclass(annotation, target_type):
203
+ return annotation, containers
204
+ if isinstance(args, tuple):
205
+ for item in args:
206
+ item_args = get_args(item)
207
+ if len(item_args) > 0:
208
+ result, container = find_annotation_in_container(item, target_type)
209
+ containers += container
210
+ if result:
211
+ containers.append(origin)
212
+ return result, containers
213
+ if len(get_args(item)) == 0 and issubclass(item, target_type):
214
+ containers.append(origin)
215
+ return item, containers
216
+ return None, []
217
+
218
+
219
+ def shell_quote(s: str) -> str:
220
+ """Single quote fix
221
+
222
+ Args:
223
+ s (str): path to be converted
224
+
225
+ Returns:
226
+ str: path to be returned
227
+ """
228
+ return "'" + s.replace("'", "'\"'\"'") + "'"
229
+
230
+
231
+ def nice_rotated_name(path: str, stem: str, prefix: str = "rotated_") -> str:
232
+ """Map path to a new local filename, generalized for any stem."""
233
+ base = path.rstrip("/").rsplit("/", 1)[-1]
234
+ s = re.escape(stem)
235
+
236
+ if base == stem:
237
+ return f"{prefix}{stem}.log"
238
+
239
+ m = re.fullmatch(rf"{s}\.(\d+)\.gz", base)
240
+ if m:
241
+ return f"{prefix}{stem}.{m.group(1)}.gz.log"
242
+
243
+ m = re.fullmatch(rf"{s}\.(\d+)", base)
244
+ if m:
245
+ return f"{prefix}{stem}.{m.group(1)}.log"
246
+
247
+ middle = base[:-3] if base.endswith(".gz") else base
248
+ return f"{prefix}{middle}.log"
249
+
250
+
251
+ def apply_bit_mask(in_hex: str, bit_mask_hex: str) -> Optional[str]:
252
+ """Extracts bit offset from bit mask, applies the bit mask and offset.
253
+
254
+ Args:
255
+ in_hex (str): Hexadecimal input
256
+ bit_mask (str): Hexadecimal bit mask
257
+
258
+ Returns:
259
+ str: hexadecimal output after applying bit mask and offset
260
+ """
261
+ if not is_hex(hex_in=in_hex) or not is_hex(hex_in=bit_mask_hex):
262
+ return None
263
+ in_dec = hex_to_int(in_hex)
264
+ bit_mask_dec = hex_to_int(bit_mask_hex)
265
+ bit_offset = get_bit_offset(bit_mask_hex)
266
+ if in_dec is None or bit_mask_dec is None or bit_offset is None:
267
+ return None
268
+ out_dec = (in_dec & bit_mask_dec) >> bit_offset
269
+ return hex(out_dec)
270
+
271
+
272
+ def apply_bit_mask_int(in_int: int, bit_mask_int: int) -> Optional[int]:
273
+ """Extracts bit offset from bit mask, applies the bit mask and offset.
274
+
275
+ Args:
276
+ in_int (int): integer input
277
+ bit_mask_int (int): integer bit mask
278
+
279
+ Returns:
280
+ int: integer output after applying bit mask and offset
281
+ """
282
+ out_int = (in_int & bit_mask_int) >> get_bit_offset_int(bit_mask_int)
283
+ return out_int
284
+
285
+
286
+ def get_bit_offset_int(bit_mask: int) -> int:
287
+ """Extracts the bit offset from bit mask.
288
+ For ex, bit_mask = 0x0010 (hex) -> 0b00010000 (bin)
289
+ Returns bit offset of 4 (bit position of the "1")
290
+
291
+ Args:
292
+ bit_mask (int): hex bit mask
293
+
294
+ Returns:
295
+ int: bit offset
296
+ """
297
+ bit_pos = 0
298
+ while bit_mask > 0:
299
+ if bit_mask % 2 == 1:
300
+ return bit_pos
301
+ bit_mask = bit_mask >> 1
302
+ bit_pos += 1
303
+
304
+ return 0
305
+
306
+
307
+ def get_bit_offset(bit_mask: str) -> Optional[int]:
308
+ """Extracts the bit offset from bit mask.
309
+ For ex, bit_mask = "0010" (hex) -> 0b00010000 (bin)
310
+ Returns bit offset of 4 (bit position of the "1")
311
+
312
+ Args:
313
+ bit_mask (str): hex bit mask
314
+
315
+ Returns:
316
+ int: bit offset
317
+ """
318
+ bit_mask_int = hex_to_int(bit_mask)
319
+ bit_pos = 0
320
+ if bit_mask_int is None:
321
+ return None
322
+ while bit_mask_int > 0:
323
+ if bit_mask_int % 2 == 1:
324
+ return bit_pos
325
+ bit_mask_int = bit_mask_int >> 1
326
+ bit_pos += 1
327
+
328
+ return 0
329
+
330
+
331
+ def get_all_subclasses(cls: Type[T]) -> Set[Type[T]]:
332
+ """Get an iterable with all subclasses of this class (not including this class)
333
+ Subclasses are presented in no particular order
334
+
335
+ Returns:
336
+ An iterable of all subclasses of this class
337
+ """
338
+ subclasses: Set[Type[T]] = set()
339
+ for subclass in cls.__subclasses__():
340
+ subclasses = subclasses.union(get_all_subclasses(subclass))
341
+ if not inspect.isabstract(subclass):
342
+ subclasses.add(subclass)
343
+ return subclasses
344
+
345
+
346
+ def get_subclass(
347
+ class_name: str, class_type: Type[T], sub_classes: Optional[List[Type[T]]]
348
+ ) -> Optional[Type[T]]:
349
+ """get a subclass with a given name
350
+
351
+ Args:
352
+ class_name (str): target sub class name
353
+ class_type (Type[T]): class type
354
+ sub_classes (Optional[List[Type[T]]]): list of sub classes to check
355
+
356
+ Returns:
357
+ Optional[Type[T]]: sub class or None if no sub class with target name is found
358
+ """
359
+ if not sub_classes:
360
+ sub_classes = list(get_all_subclasses(class_type))
361
+
362
+ for sub_class in sub_classes:
363
+ if sub_class.__name__ == class_name:
364
+ return sub_class
365
+ return None
366
+
367
+
368
+ def hex_to_int(hex_in: str) -> Optional[int]:
369
+ """Converts given hex string to int
370
+
371
+ Args:
372
+ hex_in: hexadecimal string
373
+
374
+ Returns:
375
+ int: hexadecimal converted to int
376
+ """
377
+ try:
378
+ if not is_hex(hex_in):
379
+ return None
380
+ return int(hex_in, 16)
381
+ except TypeError:
382
+ return None
383
+
384
+
385
+ def is_hex(hex_in: str) -> bool:
386
+ """Returns True or False based on whether the input hexadecimal is indeed hexadecimal
387
+
388
+ Args:
389
+ hex_in: hexadecimal string
390
+
391
+ Returns:
392
+ bool: True/False whether the input hexadecimal is indeed hexadecimal
393
+ """
394
+ if not hex_in:
395
+ return False
396
+
397
+ hex_pattern = re.compile(r"^(0x)?[0-9a-fA-F]+$")
398
+ return bool(hex_pattern.fullmatch(hex_in))
399
+
400
+
401
+ def strip_ansi_codes(text: str) -> str:
402
+ """
403
+ Remove ANSI escape codes from text.
404
+
405
+ Args:
406
+ text (str): The text string containing ANSI escape codes.
407
+
408
+ Returns:
409
+ str: The text with ANSI escape codes removed.
410
+ """
411
+ ansi_escape = re.compile(r"\x1b\[[0-9;]*m")
412
+ return ansi_escape.sub("", text)