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.
- amd_node_scraper-0.0.1.dist-info/LICENSE +21 -0
- amd_node_scraper-0.0.1.dist-info/METADATA +424 -0
- amd_node_scraper-0.0.1.dist-info/RECORD +197 -0
- amd_node_scraper-0.0.1.dist-info/WHEEL +5 -0
- amd_node_scraper-0.0.1.dist-info/entry_points.txt +2 -0
- amd_node_scraper-0.0.1.dist-info/top_level.txt +1 -0
- nodescraper/__init__.py +32 -0
- nodescraper/base/__init__.py +34 -0
- nodescraper/base/inbandcollectortask.py +118 -0
- nodescraper/base/inbanddataplugin.py +39 -0
- nodescraper/base/regexanalyzer.py +120 -0
- nodescraper/cli/__init__.py +29 -0
- nodescraper/cli/cli.py +511 -0
- nodescraper/cli/constants.py +27 -0
- nodescraper/cli/dynamicparserbuilder.py +171 -0
- nodescraper/cli/helper.py +517 -0
- nodescraper/cli/inputargtypes.py +129 -0
- nodescraper/configbuilder.py +123 -0
- nodescraper/configregistry.py +66 -0
- nodescraper/configs/node_status.json +19 -0
- nodescraper/connection/__init__.py +25 -0
- nodescraper/connection/inband/__init__.py +46 -0
- nodescraper/connection/inband/inband.py +171 -0
- nodescraper/connection/inband/inbandlocal.py +93 -0
- nodescraper/connection/inband/inbandmanager.py +151 -0
- nodescraper/connection/inband/inbandremote.py +173 -0
- nodescraper/connection/inband/sshparams.py +43 -0
- nodescraper/constants.py +26 -0
- nodescraper/enums/__init__.py +40 -0
- nodescraper/enums/eventcategory.py +89 -0
- nodescraper/enums/eventpriority.py +42 -0
- nodescraper/enums/executionstatus.py +44 -0
- nodescraper/enums/osfamily.py +34 -0
- nodescraper/enums/systeminteraction.py +41 -0
- nodescraper/enums/systemlocation.py +33 -0
- nodescraper/generictypes.py +36 -0
- nodescraper/interfaces/__init__.py +44 -0
- nodescraper/interfaces/connectionmanager.py +143 -0
- nodescraper/interfaces/dataanalyzertask.py +138 -0
- nodescraper/interfaces/datacollectortask.py +185 -0
- nodescraper/interfaces/dataplugin.py +356 -0
- nodescraper/interfaces/plugin.py +127 -0
- nodescraper/interfaces/resultcollator.py +56 -0
- nodescraper/interfaces/task.py +164 -0
- nodescraper/interfaces/taskresulthook.py +39 -0
- nodescraper/models/__init__.py +48 -0
- nodescraper/models/analyzerargs.py +93 -0
- nodescraper/models/collectorargs.py +30 -0
- nodescraper/models/connectionconfig.py +34 -0
- nodescraper/models/datamodel.py +171 -0
- nodescraper/models/datapluginresult.py +39 -0
- nodescraper/models/event.py +158 -0
- nodescraper/models/pluginconfig.py +38 -0
- nodescraper/models/pluginresult.py +39 -0
- nodescraper/models/systeminfo.py +44 -0
- nodescraper/models/taskresult.py +185 -0
- nodescraper/models/timerangeargs.py +38 -0
- nodescraper/pluginexecutor.py +274 -0
- nodescraper/pluginregistry.py +152 -0
- nodescraper/plugins/__init__.py +25 -0
- nodescraper/plugins/inband/__init__.py +25 -0
- nodescraper/plugins/inband/amdsmi/__init__.py +28 -0
- nodescraper/plugins/inband/amdsmi/amdsmi_analyzer.py +821 -0
- nodescraper/plugins/inband/amdsmi/amdsmi_collector.py +1313 -0
- nodescraper/plugins/inband/amdsmi/amdsmi_plugin.py +43 -0
- nodescraper/plugins/inband/amdsmi/amdsmidata.py +1002 -0
- nodescraper/plugins/inband/amdsmi/analyzer_args.py +50 -0
- nodescraper/plugins/inband/amdsmi/cper.py +65 -0
- nodescraper/plugins/inband/bios/__init__.py +29 -0
- nodescraper/plugins/inband/bios/analyzer_args.py +64 -0
- nodescraper/plugins/inband/bios/bios_analyzer.py +93 -0
- nodescraper/plugins/inband/bios/bios_collector.py +93 -0
- nodescraper/plugins/inband/bios/bios_plugin.py +43 -0
- nodescraper/plugins/inband/bios/biosdata.py +30 -0
- nodescraper/plugins/inband/cmdline/__init__.py +25 -0
- nodescraper/plugins/inband/cmdline/analyzer_args.py +80 -0
- nodescraper/plugins/inband/cmdline/cmdline_analyzer.py +113 -0
- nodescraper/plugins/inband/cmdline/cmdline_collector.py +77 -0
- nodescraper/plugins/inband/cmdline/cmdline_plugin.py +43 -0
- nodescraper/plugins/inband/cmdline/cmdlinedata.py +30 -0
- nodescraper/plugins/inband/device_enumeration/__init__.py +29 -0
- nodescraper/plugins/inband/device_enumeration/analyzer_args.py +73 -0
- nodescraper/plugins/inband/device_enumeration/device_enumeration_analyzer.py +81 -0
- nodescraper/plugins/inband/device_enumeration/device_enumeration_collector.py +176 -0
- nodescraper/plugins/inband/device_enumeration/device_enumeration_plugin.py +45 -0
- nodescraper/plugins/inband/device_enumeration/deviceenumdata.py +36 -0
- nodescraper/plugins/inband/dimm/__init__.py +25 -0
- nodescraper/plugins/inband/dimm/collector_args.py +31 -0
- nodescraper/plugins/inband/dimm/dimm_collector.py +151 -0
- nodescraper/plugins/inband/dimm/dimm_plugin.py +40 -0
- nodescraper/plugins/inband/dimm/dimmdata.py +30 -0
- nodescraper/plugins/inband/dkms/__init__.py +25 -0
- nodescraper/plugins/inband/dkms/analyzer_args.py +85 -0
- nodescraper/plugins/inband/dkms/dkms_analyzer.py +106 -0
- nodescraper/plugins/inband/dkms/dkms_collector.py +76 -0
- nodescraper/plugins/inband/dkms/dkms_plugin.py +43 -0
- nodescraper/plugins/inband/dkms/dkmsdata.py +33 -0
- nodescraper/plugins/inband/dmesg/__init__.py +28 -0
- nodescraper/plugins/inband/dmesg/analyzer_args.py +33 -0
- nodescraper/plugins/inband/dmesg/collector_args.py +39 -0
- nodescraper/plugins/inband/dmesg/dmesg_analyzer.py +503 -0
- nodescraper/plugins/inband/dmesg/dmesg_collector.py +164 -0
- nodescraper/plugins/inband/dmesg/dmesg_plugin.py +44 -0
- nodescraper/plugins/inband/dmesg/dmesgdata.py +116 -0
- nodescraper/plugins/inband/fabrics/__init__.py +28 -0
- nodescraper/plugins/inband/fabrics/fabrics_collector.py +726 -0
- nodescraper/plugins/inband/fabrics/fabrics_plugin.py +37 -0
- nodescraper/plugins/inband/fabrics/fabricsdata.py +140 -0
- nodescraper/plugins/inband/journal/__init__.py +28 -0
- nodescraper/plugins/inband/journal/collector_args.py +33 -0
- nodescraper/plugins/inband/journal/journal_collector.py +107 -0
- nodescraper/plugins/inband/journal/journal_plugin.py +40 -0
- nodescraper/plugins/inband/journal/journaldata.py +44 -0
- nodescraper/plugins/inband/kernel/__init__.py +25 -0
- nodescraper/plugins/inband/kernel/analyzer_args.py +64 -0
- nodescraper/plugins/inband/kernel/kernel_analyzer.py +91 -0
- nodescraper/plugins/inband/kernel/kernel_collector.py +129 -0
- nodescraper/plugins/inband/kernel/kernel_plugin.py +43 -0
- nodescraper/plugins/inband/kernel/kerneldata.py +32 -0
- nodescraper/plugins/inband/kernel_module/__init__.py +25 -0
- nodescraper/plugins/inband/kernel_module/analyzer_args.py +59 -0
- nodescraper/plugins/inband/kernel_module/kernel_module_analyzer.py +211 -0
- nodescraper/plugins/inband/kernel_module/kernel_module_collector.py +264 -0
- nodescraper/plugins/inband/kernel_module/kernel_module_data.py +60 -0
- nodescraper/plugins/inband/kernel_module/kernel_module_plugin.py +43 -0
- nodescraper/plugins/inband/memory/__init__.py +25 -0
- nodescraper/plugins/inband/memory/analyzer_args.py +45 -0
- nodescraper/plugins/inband/memory/memory_analyzer.py +98 -0
- nodescraper/plugins/inband/memory/memory_collector.py +330 -0
- nodescraper/plugins/inband/memory/memory_plugin.py +43 -0
- nodescraper/plugins/inband/memory/memorydata.py +90 -0
- nodescraper/plugins/inband/network/__init__.py +28 -0
- nodescraper/plugins/inband/network/network_collector.py +1828 -0
- nodescraper/plugins/inband/network/network_plugin.py +37 -0
- nodescraper/plugins/inband/network/networkdata.py +319 -0
- nodescraper/plugins/inband/nvme/__init__.py +28 -0
- nodescraper/plugins/inband/nvme/nvme_collector.py +167 -0
- nodescraper/plugins/inband/nvme/nvme_plugin.py +37 -0
- nodescraper/plugins/inband/nvme/nvmedata.py +45 -0
- nodescraper/plugins/inband/os/__init__.py +25 -0
- nodescraper/plugins/inband/os/analyzer_args.py +64 -0
- nodescraper/plugins/inband/os/os_analyzer.py +73 -0
- nodescraper/plugins/inband/os/os_collector.py +131 -0
- nodescraper/plugins/inband/os/os_plugin.py +43 -0
- nodescraper/plugins/inband/os/osdata.py +31 -0
- nodescraper/plugins/inband/package/__init__.py +25 -0
- nodescraper/plugins/inband/package/analyzer_args.py +48 -0
- nodescraper/plugins/inband/package/package_analyzer.py +253 -0
- nodescraper/plugins/inband/package/package_collector.py +273 -0
- nodescraper/plugins/inband/package/package_plugin.py +43 -0
- nodescraper/plugins/inband/package/packagedata.py +41 -0
- nodescraper/plugins/inband/pcie/__init__.py +29 -0
- nodescraper/plugins/inband/pcie/analyzer_args.py +63 -0
- nodescraper/plugins/inband/pcie/pcie_analyzer.py +1081 -0
- nodescraper/plugins/inband/pcie/pcie_collector.py +690 -0
- nodescraper/plugins/inband/pcie/pcie_data.py +2017 -0
- nodescraper/plugins/inband/pcie/pcie_plugin.py +43 -0
- nodescraper/plugins/inband/process/__init__.py +25 -0
- nodescraper/plugins/inband/process/analyzer_args.py +45 -0
- nodescraper/plugins/inband/process/collector_args.py +31 -0
- nodescraper/plugins/inband/process/process_analyzer.py +91 -0
- nodescraper/plugins/inband/process/process_collector.py +115 -0
- nodescraper/plugins/inband/process/process_plugin.py +46 -0
- nodescraper/plugins/inband/process/processdata.py +34 -0
- nodescraper/plugins/inband/rocm/__init__.py +25 -0
- nodescraper/plugins/inband/rocm/analyzer_args.py +66 -0
- nodescraper/plugins/inband/rocm/rocm_analyzer.py +100 -0
- nodescraper/plugins/inband/rocm/rocm_collector.py +205 -0
- nodescraper/plugins/inband/rocm/rocm_plugin.py +43 -0
- nodescraper/plugins/inband/rocm/rocmdata.py +62 -0
- nodescraper/plugins/inband/storage/__init__.py +25 -0
- nodescraper/plugins/inband/storage/analyzer_args.py +38 -0
- nodescraper/plugins/inband/storage/collector_args.py +31 -0
- nodescraper/plugins/inband/storage/storage_analyzer.py +152 -0
- nodescraper/plugins/inband/storage/storage_collector.py +110 -0
- nodescraper/plugins/inband/storage/storage_plugin.py +44 -0
- nodescraper/plugins/inband/storage/storagedata.py +70 -0
- nodescraper/plugins/inband/sysctl/__init__.py +29 -0
- nodescraper/plugins/inband/sysctl/analyzer_args.py +67 -0
- nodescraper/plugins/inband/sysctl/sysctl_analyzer.py +81 -0
- nodescraper/plugins/inband/sysctl/sysctl_collector.py +101 -0
- nodescraper/plugins/inband/sysctl/sysctl_plugin.py +43 -0
- nodescraper/plugins/inband/sysctl/sysctldata.py +42 -0
- nodescraper/plugins/inband/syslog/__init__.py +28 -0
- nodescraper/plugins/inband/syslog/syslog_collector.py +121 -0
- nodescraper/plugins/inband/syslog/syslog_plugin.py +37 -0
- nodescraper/plugins/inband/syslog/syslogdata.py +46 -0
- nodescraper/plugins/inband/uptime/__init__.py +25 -0
- nodescraper/plugins/inband/uptime/uptime_collector.py +88 -0
- nodescraper/plugins/inband/uptime/uptime_plugin.py +37 -0
- nodescraper/plugins/inband/uptime/uptimedata.py +31 -0
- nodescraper/resultcollators/__init__.py +25 -0
- nodescraper/resultcollators/tablesummary.py +159 -0
- nodescraper/taskresulthooks/__init__.py +28 -0
- nodescraper/taskresulthooks/filesystemloghook.py +88 -0
- nodescraper/typeutils.py +171 -0
- nodescraper/utils.py +412 -0
nodescraper/typeutils.py
ADDED
|
@@ -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)
|