hypster 0.0.1a0__tar.gz
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.
hypster-0.0.1a0/PKG-INFO
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: hypster
|
|
3
|
+
Version: 0.0.1a0
|
|
4
|
+
Summary: A flexible configuration system for Python projects
|
|
5
|
+
Home-page: https://github.com/hypster-dev/hypster
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: configuration,ai,machine-learning
|
|
8
|
+
Author: Gilad Rubin
|
|
9
|
+
Author-email: gilad.rubin@gmail.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Project-URL: Repository, https://github.com/hypster-dev/hypster
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<img src="assets/hypster_with_text.png" alt="Hypster Logo" width="600"/>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
Hypster is a lightweight configuration system for AI & Machine Learning projects.
|
|
24
|
+
It offers minimal, intuitive syntax, supporting hierarchical and swappable configurations with lazy instantiation - making it both powerful and easy to integrate with existing projects.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
You can install Hypster using pip:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install hypster
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
Here's a simple example of how to use Hypster:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
%%writefile configs.py
|
|
40
|
+
from hypster import lazy, Options
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class DatabaseConfig:
|
|
44
|
+
host: str
|
|
45
|
+
port: int
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class CacheConfig:
|
|
49
|
+
type: str
|
|
50
|
+
|
|
51
|
+
# "lazy" defers instantiation
|
|
52
|
+
lazy([Database, Cache], update_globals=True)
|
|
53
|
+
|
|
54
|
+
# Define configuration options
|
|
55
|
+
db_host = Options({"production": "prod.example.com",
|
|
56
|
+
"staging": "staging.example.com"}, default="staging")
|
|
57
|
+
cache_type = Options(["memory", "redis"], default="memory")
|
|
58
|
+
db_port = Options({"main": 5432, "alt": 5433}, default="main")
|
|
59
|
+
|
|
60
|
+
# Create lazy instances
|
|
61
|
+
db = Database(host=db_host, port=db_port)
|
|
62
|
+
cache = Cache(type=cache_type)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Now, in another cell or module, you can instantiate the configuration:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from hypster import Composer
|
|
69
|
+
import configs
|
|
70
|
+
|
|
71
|
+
config = Composer().with_modules(configs).compose()
|
|
72
|
+
|
|
73
|
+
result = config.instantiate(
|
|
74
|
+
final_vars=["db", "cache"],
|
|
75
|
+
selections={"db.host": "production", "cache.type": "redis"},
|
|
76
|
+
overrides={"db.port": 8000}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
db.connect() # Outputs: Connecting to prod.example.com:5434
|
|
80
|
+
cache.initialize() # Outputs: Initializing redis cache
|
|
81
|
+
```
|
|
82
|
+
## Inspiration
|
|
83
|
+
Hypster draws inspiration from [Meta's Hydra](https://github.com/facebookresearch/hydra) and [hydra-zen](https://github.com/mit-ll-responsible-ai/hydra-zen) packages, combining their powerful configuration management with a minimalist approach.
|
|
84
|
+
|
|
85
|
+
The API design is also influenced by the elegant simplicity of [Hamilton's API](https://github.com/DAGWorks-Inc/hamilton).
|
|
86
|
+
|
|
87
|
+
## Contributing
|
|
88
|
+
|
|
89
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/hypster_with_text.png" alt="Hypster Logo" width="600"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
Hypster is a lightweight configuration system for AI & Machine Learning projects.
|
|
6
|
+
It offers minimal, intuitive syntax, supporting hierarchical and swappable configurations with lazy instantiation - making it both powerful and easy to integrate with existing projects.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
You can install Hypster using pip:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install hypster
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
Here's a simple example of how to use Hypster:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
%%writefile configs.py
|
|
22
|
+
from hypster import lazy, Options
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class DatabaseConfig:
|
|
26
|
+
host: str
|
|
27
|
+
port: int
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class CacheConfig:
|
|
31
|
+
type: str
|
|
32
|
+
|
|
33
|
+
# "lazy" defers instantiation
|
|
34
|
+
lazy([Database, Cache], update_globals=True)
|
|
35
|
+
|
|
36
|
+
# Define configuration options
|
|
37
|
+
db_host = Options({"production": "prod.example.com",
|
|
38
|
+
"staging": "staging.example.com"}, default="staging")
|
|
39
|
+
cache_type = Options(["memory", "redis"], default="memory")
|
|
40
|
+
db_port = Options({"main": 5432, "alt": 5433}, default="main")
|
|
41
|
+
|
|
42
|
+
# Create lazy instances
|
|
43
|
+
db = Database(host=db_host, port=db_port)
|
|
44
|
+
cache = Cache(type=cache_type)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Now, in another cell or module, you can instantiate the configuration:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from hypster import Composer
|
|
51
|
+
import configs
|
|
52
|
+
|
|
53
|
+
config = Composer().with_modules(configs).compose()
|
|
54
|
+
|
|
55
|
+
result = config.instantiate(
|
|
56
|
+
final_vars=["db", "cache"],
|
|
57
|
+
selections={"db.host": "production", "cache.type": "redis"},
|
|
58
|
+
overrides={"db.port": 8000}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
db.connect() # Outputs: Connecting to prod.example.com:5434
|
|
62
|
+
cache.initialize() # Outputs: Initializing redis cache
|
|
63
|
+
```
|
|
64
|
+
## Inspiration
|
|
65
|
+
Hypster draws inspiration from [Meta's Hydra](https://github.com/facebookresearch/hydra) and [hydra-zen](https://github.com/mit-ll-responsible-ai/hydra-zen) packages, combining their powerful configuration management with a minimalist approach.
|
|
66
|
+
|
|
67
|
+
The API design is also influenced by the elegant simplicity of [Hamilton's API](https://github.com/DAGWorks-Inc/hamilton).
|
|
68
|
+
|
|
69
|
+
## Contributing
|
|
70
|
+
|
|
71
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "hypster"
|
|
3
|
+
version = "0.0.1a0"
|
|
4
|
+
description = "A flexible configuration system for Python projects"
|
|
5
|
+
|
|
6
|
+
authors = ["Gilad Rubin <gilad.rubin@gmail.com>"]
|
|
7
|
+
license = "MIT"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
repository = "https://github.com/hypster-dev/hypster"
|
|
10
|
+
keywords = ["configuration", "ai", "machine-learning"]
|
|
11
|
+
|
|
12
|
+
[tool.poetry.dependencies]
|
|
13
|
+
python = "^3.10"
|
|
14
|
+
|
|
15
|
+
[tool.poetry.dev-dependencies]
|
|
16
|
+
pytest = "^6.0"
|
|
17
|
+
ruff = "^0.1.0"
|
|
18
|
+
mypy = "^0.950"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["poetry-core>=1.0.0"]
|
|
22
|
+
build-backend = "poetry.core.masonry.api"
|
|
23
|
+
|
|
24
|
+
[tool.ruff]
|
|
25
|
+
line-length = 120
|
|
26
|
+
target-version = "py310"
|
|
27
|
+
|
|
28
|
+
[tool.mypy]
|
|
29
|
+
ignore_missing_imports = true
|
|
30
|
+
strict_optional = true
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from typing import Any, Dict, List, Tuple
|
|
3
|
+
|
|
4
|
+
from .logging_utils import configure_logging
|
|
5
|
+
|
|
6
|
+
logger = configure_logging()
|
|
7
|
+
|
|
8
|
+
class Select:
|
|
9
|
+
def __init__(self, lineno: int, select_index: int, explicit_name: str = None, implicit_name: str = None):
|
|
10
|
+
self.lineno = lineno
|
|
11
|
+
self.select_index = select_index
|
|
12
|
+
self.explicit_name = explicit_name
|
|
13
|
+
self.implicit_name = implicit_name
|
|
14
|
+
|
|
15
|
+
class VariableGraphBuilder(ast.NodeVisitor):
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.graph = {}
|
|
18
|
+
self.current_path = []
|
|
19
|
+
|
|
20
|
+
def visit_Assign(self, node):
|
|
21
|
+
target = self.get_target_name(node.targets[0])
|
|
22
|
+
logger.debug(f"VariableGraphBuilder: Visiting assignment to {target}")
|
|
23
|
+
self.current_path = [target]
|
|
24
|
+
self.graph[target] = self.build_subgraph(node.value)
|
|
25
|
+
|
|
26
|
+
def build_subgraph(self, node):
|
|
27
|
+
logger.debug(f"VariableGraphBuilder: Building subgraph for node type {type(node).__name__}")
|
|
28
|
+
if isinstance(node, ast.Dict):
|
|
29
|
+
return {self.get_key_name(k): self.build_subgraph(v) for k, v in zip(node.keys, node.values)}
|
|
30
|
+
elif isinstance(node, ast.Call):
|
|
31
|
+
return {
|
|
32
|
+
'__call__': self.get_target_name(node.func),
|
|
33
|
+
'args': [self.get_node_value(arg) for arg in node.args],
|
|
34
|
+
'kwargs': {kw.arg: self.get_node_value(kw.value) for kw in node.keywords}
|
|
35
|
+
}
|
|
36
|
+
elif isinstance(node, (ast.Name, ast.Attribute)):
|
|
37
|
+
return self.get_target_name(node)
|
|
38
|
+
elif isinstance(node, ast.Constant):
|
|
39
|
+
return node.value
|
|
40
|
+
else:
|
|
41
|
+
return {'__unknown__': ast.unparse(node)}
|
|
42
|
+
|
|
43
|
+
def get_node_value(self, node):
|
|
44
|
+
if isinstance(node, ast.Constant):
|
|
45
|
+
return node.value
|
|
46
|
+
elif isinstance(node, (ast.Name, ast.Attribute)):
|
|
47
|
+
return self.get_target_name(node)
|
|
48
|
+
else:
|
|
49
|
+
return ast.unparse(node)
|
|
50
|
+
|
|
51
|
+
def get_target_name(self, node):
|
|
52
|
+
if isinstance(node, ast.Name):
|
|
53
|
+
return node.id
|
|
54
|
+
elif isinstance(node, ast.Attribute):
|
|
55
|
+
return f"{self.get_target_name(node.value)}.{node.attr}"
|
|
56
|
+
else:
|
|
57
|
+
return "Unknown"
|
|
58
|
+
|
|
59
|
+
def get_key_name(self, node):
|
|
60
|
+
if isinstance(node, ast.Str):
|
|
61
|
+
return node.s
|
|
62
|
+
return self.get_target_name(node)
|
|
63
|
+
|
|
64
|
+
class HPSelectAnalyzer(ast.NodeVisitor):
|
|
65
|
+
def __init__(self, variable_graph):
|
|
66
|
+
self.variable_graph = variable_graph
|
|
67
|
+
self.results = {}
|
|
68
|
+
self.current_assignment = None
|
|
69
|
+
self.current_complex_assignment = None
|
|
70
|
+
self.selects = []
|
|
71
|
+
|
|
72
|
+
def visit_Assign(self, node):
|
|
73
|
+
logger.debug(f"HPSelectAnalyzer: Visiting assignment on line {node.lineno}")
|
|
74
|
+
self.current_assignment = node
|
|
75
|
+
start_line = node.lineno
|
|
76
|
+
end_line = self.get_last_line(node)
|
|
77
|
+
|
|
78
|
+
if isinstance(node.value, (ast.Dict, ast.Call)):
|
|
79
|
+
self.current_complex_assignment = node
|
|
80
|
+
|
|
81
|
+
self.results[start_line] = {'code': ast.unparse(node), 'selects': [], 'end_line': end_line}
|
|
82
|
+
self.generic_visit(node)
|
|
83
|
+
|
|
84
|
+
self.current_complex_assignment = None
|
|
85
|
+
self.current_assignment = None
|
|
86
|
+
|
|
87
|
+
def visit_Call(self, node):
|
|
88
|
+
if self.is_hp_select(node):
|
|
89
|
+
logger.debug(f"HPSelectAnalyzer: hp.select call detected on line {node.lineno}")
|
|
90
|
+
line_number = node.lineno
|
|
91
|
+
select_index = len([s for s in self.selects if s.lineno == line_number])
|
|
92
|
+
|
|
93
|
+
explicit_name = self.get_hp_select_name(node)
|
|
94
|
+
inferred_name = self.infer_name(node)
|
|
95
|
+
|
|
96
|
+
if not explicit_name and inferred_name and self.is_valid_inference(inferred_name):
|
|
97
|
+
implicit_name = inferred_name
|
|
98
|
+
else:
|
|
99
|
+
implicit_name = None
|
|
100
|
+
|
|
101
|
+
select = Select(line_number, select_index, explicit_name, implicit_name)
|
|
102
|
+
self.selects.append(select)
|
|
103
|
+
|
|
104
|
+
if self.current_complex_assignment and self.current_complex_assignment.lineno <= line_number <= self.get_last_line(self.current_complex_assignment):
|
|
105
|
+
logger.debug(f"HPSelectAnalyzer: hp.select is part of a complex assignment starting on line {self.current_complex_assignment.lineno}")
|
|
106
|
+
line_number = self.current_complex_assignment.lineno
|
|
107
|
+
|
|
108
|
+
if line_number in self.results:
|
|
109
|
+
self.results[line_number]['selects'].append(select)
|
|
110
|
+
else:
|
|
111
|
+
self.results[line_number] = {
|
|
112
|
+
'code': ast.unparse(self.current_complex_assignment or node),
|
|
113
|
+
'selects': [select],
|
|
114
|
+
'end_line': self.get_last_line(self.current_complex_assignment or node)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
self.generic_visit(node)
|
|
118
|
+
|
|
119
|
+
def is_valid_inference(self, inferred_name):
|
|
120
|
+
logger.debug(f"HPSelectAnalyzer: Checking validity of inferred name: {inferred_name}")
|
|
121
|
+
parts = inferred_name.split('.')
|
|
122
|
+
valid = len(parts) <= 4 and all(not part.startswith('arg') for part in parts)
|
|
123
|
+
logger.debug(f"HPSelectAnalyzer: Inferred name {'is' if valid else 'is not'} valid")
|
|
124
|
+
return valid
|
|
125
|
+
|
|
126
|
+
def get_last_line(self, node):
|
|
127
|
+
return max(getattr(node, 'lineno', 0), getattr(node, 'end_lineno', 0))
|
|
128
|
+
|
|
129
|
+
def is_hp_select(self, node):
|
|
130
|
+
return (isinstance(node.func, ast.Attribute) and
|
|
131
|
+
isinstance(node.func.value, ast.Name) and
|
|
132
|
+
node.func.value.id == 'hp' and
|
|
133
|
+
node.func.attr == 'select')
|
|
134
|
+
|
|
135
|
+
def get_hp_select_name(self, node):
|
|
136
|
+
logger.debug(f"HPSelectAnalyzer: Attempting to get hp.select name for node on line {node.lineno}")
|
|
137
|
+
if len(node.args) >= 2:
|
|
138
|
+
name = self.get_node_value(node.args[1])
|
|
139
|
+
logger.debug(f"HPSelectAnalyzer: Found name in second argument: {name}")
|
|
140
|
+
return name
|
|
141
|
+
for keyword in node.keywords:
|
|
142
|
+
if keyword.arg == 'name':
|
|
143
|
+
name = self.get_node_value(keyword.value)
|
|
144
|
+
logger.debug(f"HPSelectAnalyzer: Found name in keyword argument: {name}")
|
|
145
|
+
return name
|
|
146
|
+
logger.debug("HPSelectAnalyzer: No explicit name found for hp.select")
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
def get_node_value(self, node):
|
|
150
|
+
if isinstance(node, ast.Constant):
|
|
151
|
+
return node.value
|
|
152
|
+
elif isinstance(node, ast.Name):
|
|
153
|
+
return node.id
|
|
154
|
+
else:
|
|
155
|
+
return ast.unparse(node)
|
|
156
|
+
|
|
157
|
+
def infer_name(self, node):
|
|
158
|
+
logger.debug(f"HPSelectAnalyzer: Attempting to infer name for node on line {node.lineno}")
|
|
159
|
+
if self.current_assignment:
|
|
160
|
+
target = self.get_target_name(self.current_assignment.targets[0])
|
|
161
|
+
if isinstance(self.current_assignment.value, ast.Call) and self.is_hp_select(self.current_assignment.value):
|
|
162
|
+
logger.debug(f"HPSelectAnalyzer: Direct assignment to hp.select detected: {target}")
|
|
163
|
+
return target
|
|
164
|
+
inferred = self.find_node_in_assignment(self.current_assignment.value, node, [target])
|
|
165
|
+
logger.debug(f"HPSelectAnalyzer: Inferred name from current assignment: {inferred}")
|
|
166
|
+
return inferred
|
|
167
|
+
|
|
168
|
+
for var, subgraph in self.variable_graph.items():
|
|
169
|
+
path = self.find_node_in_graph(subgraph, node)
|
|
170
|
+
if path:
|
|
171
|
+
inferred = '.'.join([var] + [p for p in path if p != 'kwargs'])
|
|
172
|
+
logger.debug(f"HPSelectAnalyzer: Inferred name from variable graph: {inferred}")
|
|
173
|
+
return inferred
|
|
174
|
+
logger.debug("HPSelectAnalyzer: Unable to infer name")
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def find_node_in_assignment(self, value_node, target_node, path):
|
|
178
|
+
logger.debug(f"HPSelectAnalyzer: Searching for node in assignment, current path: {'.'.join(path)}")
|
|
179
|
+
if isinstance(value_node, ast.Dict):
|
|
180
|
+
for key, value in zip(value_node.keys, value_node.values):
|
|
181
|
+
if value == target_node:
|
|
182
|
+
result = '.'.join(path + [self.get_node_value(key)])
|
|
183
|
+
logger.debug(f"HPSelectAnalyzer: Found node in dictionary: {result}")
|
|
184
|
+
return result
|
|
185
|
+
result = self.find_node_in_assignment(value, target_node, path + [self.get_node_value(key)])
|
|
186
|
+
if result:
|
|
187
|
+
return result
|
|
188
|
+
elif isinstance(value_node, ast.Call):
|
|
189
|
+
for idx, arg in enumerate(value_node.args):
|
|
190
|
+
if arg == target_node:
|
|
191
|
+
result = '.'.join(path + [f'arg{idx}'])
|
|
192
|
+
logger.debug(f"HPSelectAnalyzer: Found node in function argument: {result}")
|
|
193
|
+
return result
|
|
194
|
+
result = self.find_node_in_assignment(arg, target_node, path + [f'arg{idx}'])
|
|
195
|
+
if result:
|
|
196
|
+
return result
|
|
197
|
+
for keyword in value_node.keywords:
|
|
198
|
+
if keyword.value == target_node:
|
|
199
|
+
result = '.'.join(path + [keyword.arg])
|
|
200
|
+
logger.debug(f"HPSelectAnalyzer: Found node in keyword argument: {result}")
|
|
201
|
+
return result
|
|
202
|
+
result = self.find_node_in_assignment(keyword.value, target_node, path + [keyword.arg])
|
|
203
|
+
if result:
|
|
204
|
+
return result
|
|
205
|
+
logger.debug("HPSelectAnalyzer: Node not found in assignment")
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def find_node_in_graph(self, graph, node, path=None):
|
|
209
|
+
if path is None:
|
|
210
|
+
path = []
|
|
211
|
+
|
|
212
|
+
logger.debug(f"HPSelectAnalyzer: Searching for node in graph, current path: {'.'.join(path)}")
|
|
213
|
+
if isinstance(graph, dict):
|
|
214
|
+
if graph.get('__call__') == 'hp.select' and graph['args'] and self.get_node_value(node.args[0]) == graph['args'][0]:
|
|
215
|
+
logger.debug(f"HPSelectAnalyzer: Found matching hp.select in graph at path: {'.'.join(path)}")
|
|
216
|
+
return path
|
|
217
|
+
for key, value in graph.items():
|
|
218
|
+
new_path = self.find_node_in_graph(value, node, path + [key])
|
|
219
|
+
if new_path:
|
|
220
|
+
return new_path
|
|
221
|
+
elif isinstance(graph, list):
|
|
222
|
+
for i, item in enumerate(graph):
|
|
223
|
+
new_path = self.find_node_in_graph(item, node, path + [str(i)])
|
|
224
|
+
if new_path:
|
|
225
|
+
return new_path
|
|
226
|
+
logger.debug("HPSelectAnalyzer: Node not found in graph")
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def get_target_name(self, node):
|
|
230
|
+
if isinstance(node, ast.Name):
|
|
231
|
+
return node.id
|
|
232
|
+
elif isinstance(node, ast.Attribute):
|
|
233
|
+
return f"{self.get_target_name(node.value)}.{node.attr}"
|
|
234
|
+
else:
|
|
235
|
+
return "Unknown"
|
|
236
|
+
|
|
237
|
+
def analyze_hp_select(code: str) -> Tuple[Dict[int, Dict[str, Any]], List[Select]]:
|
|
238
|
+
logger.info("Starting hp.select analysis")
|
|
239
|
+
tree = ast.parse(code)
|
|
240
|
+
|
|
241
|
+
logger.debug("Building variable graph")
|
|
242
|
+
graph_builder = VariableGraphBuilder()
|
|
243
|
+
graph_builder.visit(tree)
|
|
244
|
+
|
|
245
|
+
logger.debug("Analyzing hp.select calls")
|
|
246
|
+
analyzer = HPSelectAnalyzer(graph_builder.graph)
|
|
247
|
+
analyzer.visit(tree)
|
|
248
|
+
|
|
249
|
+
logger.info("hp.select analysis complete")
|
|
250
|
+
return analyzer.results, analyzer.selects
|
|
251
|
+
|
|
252
|
+
def inject_names(source_code: str, selects: List[Select]) -> str:
|
|
253
|
+
tree = ast.parse(source_code)
|
|
254
|
+
|
|
255
|
+
class NameInjector(ast.NodeTransformer):
|
|
256
|
+
def __init__(self, selects):
|
|
257
|
+
self.selects = selects
|
|
258
|
+
self.select_index = {}
|
|
259
|
+
|
|
260
|
+
def visit_Call(self, node):
|
|
261
|
+
if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.value.id == 'hp' and node.func.attr == 'select':
|
|
262
|
+
lineno = node.lineno
|
|
263
|
+
if lineno not in self.select_index:
|
|
264
|
+
self.select_index[lineno] = 0
|
|
265
|
+
else:
|
|
266
|
+
self.select_index[lineno] += 1
|
|
267
|
+
|
|
268
|
+
select = next((s for s in self.selects if s.lineno == lineno and s.select_index == self.select_index[lineno]), None)
|
|
269
|
+
|
|
270
|
+
if select and not select.explicit_name and select.implicit_name:
|
|
271
|
+
# Inject the implicit name as a keyword argument
|
|
272
|
+
node.keywords.append(ast.keyword(arg='name', value=ast.Constant(value=select.implicit_name)))
|
|
273
|
+
|
|
274
|
+
return self.generic_visit(node)
|
|
275
|
+
|
|
276
|
+
injector = NameInjector(selects)
|
|
277
|
+
modified_tree = injector.visit(tree)
|
|
278
|
+
return ast.unparse(modified_tree)
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# core.py
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import inspect
|
|
5
|
+
import types
|
|
6
|
+
from typing import Any, Callable, Dict, List, Union, Optional
|
|
7
|
+
|
|
8
|
+
from .logging_utils import configure_logging
|
|
9
|
+
from .ast_analyzer import inject_names, analyze_hp_select
|
|
10
|
+
|
|
11
|
+
logger = configure_logging()
|
|
12
|
+
|
|
13
|
+
class HP:
|
|
14
|
+
def __init__(self, final_vars: List[str], selections: Dict[str, Any], overrides: Dict[str, Any]):
|
|
15
|
+
self.final_vars = final_vars
|
|
16
|
+
self.selections = selections
|
|
17
|
+
self.overrides = overrides
|
|
18
|
+
self.config_dict = {}
|
|
19
|
+
logger.info("Initialized HP with final_vars: %s, selections: %s, and overrides: %s",
|
|
20
|
+
self.final_vars, self.selections, self.overrides)
|
|
21
|
+
|
|
22
|
+
def select(self, options: Union[Dict[str, Any], List[Any]], name: str = None, default: Any = None):
|
|
23
|
+
if name is None:
|
|
24
|
+
raise ValueError("Name must be provided explicitly or automatically inferred.")
|
|
25
|
+
|
|
26
|
+
if isinstance(options, dict):
|
|
27
|
+
if not all(isinstance(k, str) for k in options.keys()):
|
|
28
|
+
bad_keys = [key for key in options.keys() if not isinstance(key, str)]
|
|
29
|
+
raise ValueError(f"Dictionary keys must be strings. got {bad_keys} instead.")
|
|
30
|
+
elif isinstance(options, list):
|
|
31
|
+
if not all(isinstance(v, (str, int, bool, float)) for v in options):
|
|
32
|
+
raise ValueError("List values must be one of: str, int, bool, float.")
|
|
33
|
+
options = {v: v for v in options}
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError("Options must be a dictionary or a list.")
|
|
36
|
+
|
|
37
|
+
if default is not None and default not in options:
|
|
38
|
+
raise ValueError("Default value must be one of the options.")
|
|
39
|
+
|
|
40
|
+
logger.debug("Select called with options: %s, name: %s, default: %s", options, name, default)
|
|
41
|
+
|
|
42
|
+
result = None
|
|
43
|
+
if name in self.overrides:
|
|
44
|
+
override_value = self.overrides[name]
|
|
45
|
+
logger.debug("Found override for %s: %s", name, override_value)
|
|
46
|
+
if override_value in options:
|
|
47
|
+
result = options[override_value]
|
|
48
|
+
else:
|
|
49
|
+
result = override_value
|
|
50
|
+
logger.info("Applied override for %s: %s", name, result)
|
|
51
|
+
elif name in self.selections:
|
|
52
|
+
selected_value = self.selections[name]
|
|
53
|
+
logger.debug("Found selection for %s: %s", name, selected_value)
|
|
54
|
+
if selected_value in options:
|
|
55
|
+
result = options[selected_value]
|
|
56
|
+
logger.info("Applied selection for %s: %s", name, result)
|
|
57
|
+
else:
|
|
58
|
+
raise InvalidSelectionError(
|
|
59
|
+
f"Invalid selection '{selected_value}' for '{name}'. Not in options: {list(options.keys())}"
|
|
60
|
+
)
|
|
61
|
+
elif default is not None:
|
|
62
|
+
result = options[default]
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError(f"No selection or override found for {name} and no default provided.")
|
|
65
|
+
|
|
66
|
+
self.config_dict[name] = result
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
def propagate(self, config_func: Callable, name: str) -> Dict[str, Any]:
|
|
70
|
+
logger.info(f"Propagating configuration for {name}")
|
|
71
|
+
|
|
72
|
+
# Create dictionaries for the nested configuration
|
|
73
|
+
nested_selections = {k[len(name)+1:]: v for k, v in self.selections.items() if k.startswith(f"{name}.")}
|
|
74
|
+
nested_overrides = {k[len(name)+1:]: v for k, v in self.overrides.items() if k.startswith(f"{name}.")}
|
|
75
|
+
|
|
76
|
+
# Automatically propagate final_vars
|
|
77
|
+
nested_final_vars = [var[len(name)+1:] for var in self.final_vars if var.startswith(f"{name}.")]
|
|
78
|
+
|
|
79
|
+
logger.debug(f"Propagated configuration for {name} with Selections:\n{nested_selections}\n& Overrides:\n{nested_overrides}\nAuto-propagated final vars: {nested_final_vars}")
|
|
80
|
+
result = config_func(final_vars=nested_final_vars, selections=nested_selections, overrides=nested_overrides)
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
class Hypster:
|
|
84
|
+
def __init__(self, func: Callable, source_code: str = None):
|
|
85
|
+
self.func = func
|
|
86
|
+
self.source_code = source_code or inspect.getsource(func)
|
|
87
|
+
|
|
88
|
+
def __call__(self, final_vars: List[str] = [], selections: Dict[str, Any] = {}, overrides: Dict[str, Any] = {}):
|
|
89
|
+
logger.info("Hypster called with final_vars: %s, selections: %s, overrides: %s",
|
|
90
|
+
final_vars, selections, overrides)
|
|
91
|
+
try:
|
|
92
|
+
hp = HP(final_vars, selections, overrides)
|
|
93
|
+
|
|
94
|
+
# Analyze and modify the source code
|
|
95
|
+
results, selects = analyze_hp_select(self.source_code)
|
|
96
|
+
modified_source = inject_names(self.source_code, selects)
|
|
97
|
+
|
|
98
|
+
# Extract the function body
|
|
99
|
+
function_body = self._extract_function_body(modified_source)
|
|
100
|
+
|
|
101
|
+
# Create a new namespace and add the 'hp' object to it
|
|
102
|
+
namespace = {'hp': hp}
|
|
103
|
+
|
|
104
|
+
# Execute the modified function body in this namespace
|
|
105
|
+
exec(function_body, globals(), namespace)
|
|
106
|
+
|
|
107
|
+
# Process and filter the results
|
|
108
|
+
final_result = self._process_results(namespace)
|
|
109
|
+
|
|
110
|
+
if not final_vars:
|
|
111
|
+
return final_result
|
|
112
|
+
else:
|
|
113
|
+
result = {k: final_result.get(k, None) for k in final_vars}
|
|
114
|
+
logger.debug("Final result after filtering by final_vars: %s", result)
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.error("An error occurred: %s", str(e))
|
|
119
|
+
raise
|
|
120
|
+
|
|
121
|
+
def save(self, path: str):
|
|
122
|
+
save(self, path)
|
|
123
|
+
|
|
124
|
+
def _extract_function_body(self, source: str) -> str:
|
|
125
|
+
lines = source.split('\n')
|
|
126
|
+
body_start = next(i for i, line in enumerate(lines) if line.strip().endswith(':'))
|
|
127
|
+
body_lines = lines[body_start + 1:]
|
|
128
|
+
min_indent = min(len(line) - len(line.lstrip()) for line in body_lines if line.strip())
|
|
129
|
+
return '\n'.join(line[min_indent:] for line in body_lines)
|
|
130
|
+
|
|
131
|
+
def _process_results(self, namespace: Dict[str, Any]) -> Dict[str, Any]:
|
|
132
|
+
filtered_locals = {
|
|
133
|
+
k: v for k, v in namespace.items()
|
|
134
|
+
if k != 'hp' and not k.startswith('__') and not isinstance(v, (types.ModuleType, types.FunctionType, type))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
final_result = {k: v for k, v in filtered_locals.items() if not k.startswith('_')}
|
|
138
|
+
|
|
139
|
+
logger.debug("Captured locals: %s", filtered_locals)
|
|
140
|
+
logger.debug("Final result after filtering: %s", final_result)
|
|
141
|
+
|
|
142
|
+
return final_result
|
|
143
|
+
|
|
144
|
+
def config(func: Callable) -> Hypster:
|
|
145
|
+
return Hypster(func)
|
|
146
|
+
|
|
147
|
+
def save(hypster_instance: Hypster, path: Optional[str] = None):
|
|
148
|
+
if not isinstance(hypster_instance, Hypster):
|
|
149
|
+
raise ValueError("The provided object is not a Hypster instance")
|
|
150
|
+
|
|
151
|
+
if path is None:
|
|
152
|
+
path = f"{hypster_instance.func.__name__}.py"
|
|
153
|
+
|
|
154
|
+
# Parse the source code into an AST
|
|
155
|
+
tree = ast.parse(hypster_instance.source_code)
|
|
156
|
+
|
|
157
|
+
# Find the function definition and remove decorators
|
|
158
|
+
for node in ast.walk(tree):
|
|
159
|
+
if isinstance(node, ast.FunctionDef):
|
|
160
|
+
node.decorator_list = []
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
# Convert the modified AST back to source code
|
|
164
|
+
modified_source = ast.unparse(tree)
|
|
165
|
+
|
|
166
|
+
with open(path, "w") as f:
|
|
167
|
+
f.write(modified_source)
|
|
168
|
+
|
|
169
|
+
logger.info("Configuration saved to %s", path)
|
|
170
|
+
|
|
171
|
+
def load(path: str) -> Hypster:
|
|
172
|
+
with open(path, "r") as f:
|
|
173
|
+
source = f.read()
|
|
174
|
+
|
|
175
|
+
# Execute the source code to define the function
|
|
176
|
+
namespace = {}
|
|
177
|
+
exec(source, namespace)
|
|
178
|
+
|
|
179
|
+
# Find the function in the namespace
|
|
180
|
+
for name, obj in namespace.items():
|
|
181
|
+
if callable(obj) and not name.startswith("__"):
|
|
182
|
+
# Create and return a Hypster instance with the source code
|
|
183
|
+
return Hypster(obj, source_code=source)
|
|
184
|
+
|
|
185
|
+
raise ValueError("No suitable function found in the source code")
|
|
186
|
+
|
|
187
|
+
class InvalidSelectionError(Exception):
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
# # core.py
|
|
191
|
+
|
|
192
|
+
# import ast
|
|
193
|
+
# import inspect
|
|
194
|
+
# from typing import Any, Callable, Dict, List, Union, Optional
|
|
195
|
+
|
|
196
|
+
# from .logging_utils import configure_logging
|
|
197
|
+
|
|
198
|
+
# logger = configure_logging()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# class HP:
|
|
202
|
+
# def __init__(self, selections: Dict[str, Any], overrides: Dict[str, Any]):
|
|
203
|
+
# self.selections = selections
|
|
204
|
+
# self.overrides = overrides
|
|
205
|
+
# self.config_dict = {}
|
|
206
|
+
# logger.info("Initialized HP with selections: %s and overrides: %s", self.selections, self.overrides)
|
|
207
|
+
|
|
208
|
+
# def select(self, options: Union[Dict[str, Any], List[Any]], name: str = None, default: Any = None):
|
|
209
|
+
# if name is None:
|
|
210
|
+
# raise ValueError("Name must be provided explicitly or automatically inferred.")
|
|
211
|
+
|
|
212
|
+
# if isinstance(options, dict):
|
|
213
|
+
# if not all(isinstance(k, str) for k in options.keys()):
|
|
214
|
+
# bad_keys = [key for key in options.keys() if not isinstance(key, str)]
|
|
215
|
+
# raise ValueError(f"Dictionary keys must be strings. got {bad_keys} instead.")
|
|
216
|
+
# elif isinstance(options, list):
|
|
217
|
+
# if not all(isinstance(v, (str, int, bool, float)) for v in options):
|
|
218
|
+
# raise ValueError("List values must be one of: str, int, bool, float.")
|
|
219
|
+
# options = {v: v for v in options}
|
|
220
|
+
# else:
|
|
221
|
+
# raise ValueError("Options must be a dictionary or a list.")
|
|
222
|
+
|
|
223
|
+
# if default is not None and default not in options:
|
|
224
|
+
# raise ValueError("Default value must be one of the options.")
|
|
225
|
+
|
|
226
|
+
# logger.debug("Select called with options: %s, name: %s, default: %s", options, name, default)
|
|
227
|
+
|
|
228
|
+
# result = None
|
|
229
|
+
# if name in self.overrides:
|
|
230
|
+
# override_value = self.overrides[name]
|
|
231
|
+
# logger.debug("Found override for %s: %s", name, override_value)
|
|
232
|
+
# if override_value in options:
|
|
233
|
+
# result = options[override_value]
|
|
234
|
+
# else:
|
|
235
|
+
# result = override_value
|
|
236
|
+
# logger.info("Applied override for %s: %s", name, result)
|
|
237
|
+
# elif name in self.selections:
|
|
238
|
+
# selected_value = self.selections[name]
|
|
239
|
+
# logger.debug("Found selection for %s: %s", name, selected_value)
|
|
240
|
+
# if selected_value in options:
|
|
241
|
+
# result = options[selected_value]
|
|
242
|
+
# logger.info("Applied selection for %s: %s", name, result)
|
|
243
|
+
# else:
|
|
244
|
+
# raise InvalidSelectionError(
|
|
245
|
+
# f"Invalid selection '{selected_value}' for '{name}'. Not in options: {list(options.keys())}"
|
|
246
|
+
# )
|
|
247
|
+
# elif default is not None:
|
|
248
|
+
# result = options[default]
|
|
249
|
+
# else:
|
|
250
|
+
# raise ValueError(f"No selection or override found for {name} and no default provided.")
|
|
251
|
+
|
|
252
|
+
# self.config_dict[name] = result
|
|
253
|
+
# return result
|
|
254
|
+
|
|
255
|
+
# def propagate(self, config_func: Callable, name: str) -> Dict[str, Any]:
|
|
256
|
+
# logger.info(f"Propagating configuration for {name}")
|
|
257
|
+
|
|
258
|
+
# # Create a new HP instance for the nested configuration
|
|
259
|
+
# nested_selections = {k[len(name)+1:]: v for k, v in self.selections.items() if k.startswith(f"{name}.")}
|
|
260
|
+
# nested_overrides = {k[len(name)+1:]: v for k, v in self.overrides.items() if k.startswith(f"{name}.")}
|
|
261
|
+
# #nested_hp = HP(nested_selections, nested_overrides)
|
|
262
|
+
|
|
263
|
+
# # Execute the nested configuration function
|
|
264
|
+
# logger.debug(f"Propagated configuration for {name} with Selections:\n{nested_selections}\n& Overrides:\n{nested_overrides}")
|
|
265
|
+
# return config_func(selections=nested_selections, overrides=nested_overrides)
|
|
266
|
+
|
|
267
|
+
# import ast
|
|
268
|
+
# import inspect
|
|
269
|
+
# import types
|
|
270
|
+
# from typing import Any, Callable, Dict, List, Union
|
|
271
|
+
# from .logging_utils import configure_logging
|
|
272
|
+
# from .ast_analyzer import inject_names, analyze_hp_select
|
|
273
|
+
|
|
274
|
+
# logger = configure_logging()
|
|
275
|
+
|
|
276
|
+
# class Hypster:
|
|
277
|
+
# def __init__(self, func: Callable, source_code: str = None):
|
|
278
|
+
# self.func = func
|
|
279
|
+
# self.source_code = source_code or inspect.getsource(func)
|
|
280
|
+
|
|
281
|
+
# def __call__(self, final_vars: List[str] = [], selections: Dict[str, Any] = {}, overrides: Dict[str, Any] = {}):
|
|
282
|
+
# logger.info("Hypster called with final_vars: %s, selections: %s, overrides: %s",
|
|
283
|
+
# final_vars, selections, overrides)
|
|
284
|
+
# try:
|
|
285
|
+
# hp = HP(selections, overrides)
|
|
286
|
+
|
|
287
|
+
# # Analyze and modify the source code
|
|
288
|
+
# results, selects = analyze_hp_select(self.source_code)
|
|
289
|
+
# modified_source = inject_names(self.source_code, selects)
|
|
290
|
+
|
|
291
|
+
# # Extract the function body
|
|
292
|
+
# function_body = self._extract_function_body(modified_source)
|
|
293
|
+
|
|
294
|
+
# # Create a new namespace and add the 'hp' object to it
|
|
295
|
+
# namespace = {'hp': hp}
|
|
296
|
+
|
|
297
|
+
# # Execute the modified function body in this namespace
|
|
298
|
+
# exec(function_body, globals(), namespace)
|
|
299
|
+
|
|
300
|
+
# # Process and filter the results
|
|
301
|
+
# final_result = self._process_results(namespace)
|
|
302
|
+
|
|
303
|
+
# if not final_vars:
|
|
304
|
+
# return final_result
|
|
305
|
+
# else:
|
|
306
|
+
# result = {k: final_result.get(k, None) for k in final_vars}
|
|
307
|
+
# logger.debug("Final result after filtering by final_vars: %s", result)
|
|
308
|
+
# return result
|
|
309
|
+
|
|
310
|
+
# except Exception as e:
|
|
311
|
+
# logger.error("An error occurred: %s", str(e))
|
|
312
|
+
# raise
|
|
313
|
+
|
|
314
|
+
# def save(self, path: str):
|
|
315
|
+
# save(self, path)
|
|
316
|
+
|
|
317
|
+
# def _extract_function_body(self, source: str) -> str:
|
|
318
|
+
# lines = source.split('\n')
|
|
319
|
+
# body_start = next(i for i, line in enumerate(lines) if line.strip().endswith(':'))
|
|
320
|
+
# body_lines = lines[body_start + 1:]
|
|
321
|
+
# min_indent = min(len(line) - len(line.lstrip()) for line in body_lines if line.strip())
|
|
322
|
+
# return '\n'.join(line[min_indent:] for line in body_lines)
|
|
323
|
+
|
|
324
|
+
# def _process_results(self, namespace: Dict[str, Any]) -> Dict[str, Any]:
|
|
325
|
+
# filtered_locals = {
|
|
326
|
+
# k: v for k, v in namespace.items()
|
|
327
|
+
# if k != 'hp' and not k.startswith('__') and not isinstance(v, (types.ModuleType, types.FunctionType, type))
|
|
328
|
+
# }
|
|
329
|
+
|
|
330
|
+
# final_result = {k: v for k, v in filtered_locals.items() if not k.startswith('_')}
|
|
331
|
+
|
|
332
|
+
# logger.debug("Captured locals: %s", filtered_locals)
|
|
333
|
+
# logger.debug("Final result after filtering: %s", final_result)
|
|
334
|
+
|
|
335
|
+
# return final_result
|
|
336
|
+
|
|
337
|
+
# def config(func: Callable) -> Hypster:
|
|
338
|
+
# return Hypster(func)
|
|
339
|
+
|
|
340
|
+
# def save(hypster_instance: Hypster, path: Optional[str] = None):
|
|
341
|
+
# if not isinstance(hypster_instance, Hypster):
|
|
342
|
+
# raise ValueError("The provided object is not a Hypster instance")
|
|
343
|
+
|
|
344
|
+
# if path is None:
|
|
345
|
+
# path = f"{hypster_instance.func.__name__}.py"
|
|
346
|
+
|
|
347
|
+
# # Parse the source code into an AST
|
|
348
|
+
# tree = ast.parse(hypster_instance.source_code)
|
|
349
|
+
|
|
350
|
+
# # Find the function definition and remove decorators
|
|
351
|
+
# for node in ast.walk(tree):
|
|
352
|
+
# if isinstance(node, ast.FunctionDef):
|
|
353
|
+
# node.decorator_list = []
|
|
354
|
+
# break
|
|
355
|
+
|
|
356
|
+
# # Convert the modified AST back to source code
|
|
357
|
+
# modified_source = ast.unparse(tree)
|
|
358
|
+
|
|
359
|
+
# with open(path, "w") as f:
|
|
360
|
+
# f.write(modified_source)
|
|
361
|
+
|
|
362
|
+
# logger.info("Configuration saved to %s", path)
|
|
363
|
+
|
|
364
|
+
# def load(path: str) -> Hypster:
|
|
365
|
+
# with open(path, "r") as f:
|
|
366
|
+
# source = f.read()
|
|
367
|
+
|
|
368
|
+
# # Execute the source code to define the function
|
|
369
|
+
# namespace = {}
|
|
370
|
+
# exec(source, namespace)
|
|
371
|
+
|
|
372
|
+
# # Find the function in the namespace
|
|
373
|
+
# for name, obj in namespace.items():
|
|
374
|
+
# if callable(obj) and not name.startswith("__"):
|
|
375
|
+
# # Create and return a Hypster instance with the source code
|
|
376
|
+
# return Hypster(obj, source_code=source)
|
|
377
|
+
|
|
378
|
+
# raise ValueError("No suitable function found in the source code")
|
|
379
|
+
|
|
380
|
+
# class InvalidSelectionError(Exception):
|
|
381
|
+
# pass
|
|
382
|
+
|
|
383
|
+
# # Example usage (can be commented out in the actual module)
|
|
384
|
+
# """
|
|
385
|
+
# @config
|
|
386
|
+
# def my_config(hp):
|
|
387
|
+
# hp.select(["a", "b", "c"], name="a", default="a")
|
|
388
|
+
# hp.select({"x": 1, "y": 2}, name="b", default="x")
|
|
389
|
+
|
|
390
|
+
# # Save the configuration
|
|
391
|
+
# save(my_config, "my_config.py")
|
|
392
|
+
|
|
393
|
+
# # Load the configuration
|
|
394
|
+
# loaded_config = load("my_config.py")
|
|
395
|
+
|
|
396
|
+
# # Use the loaded configuration
|
|
397
|
+
# result = loaded_config(final_vars=["a"], selections={"b": "y"}, overrides={"a": "c"})
|
|
398
|
+
# print(result)
|
|
399
|
+
# """
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
class CustomFormatter(logging.Formatter):
|
|
4
|
+
BLACK = "\033[0;30m"
|
|
5
|
+
RED = "\033[0;31m"
|
|
6
|
+
GREEN = "\033[0;32m"
|
|
7
|
+
BROWN = "\033[0;33m"
|
|
8
|
+
BLUE = "\033[0;34m"
|
|
9
|
+
PURPLE = "\033[0;35m"
|
|
10
|
+
CYAN = "\033[0;36m"
|
|
11
|
+
LIGHT_GRAY = "\033[0;37m"
|
|
12
|
+
DARK_GRAY = "\033[1;30m"
|
|
13
|
+
LIGHT_RED = "\033[1;31m"
|
|
14
|
+
LIGHT_GREEN = "\033[1;32m"
|
|
15
|
+
YELLOW = "\033[1;33m"
|
|
16
|
+
LIGHT_BLUE = "\033[1;34m"
|
|
17
|
+
LIGHT_PURPLE = "\033[1;35m"
|
|
18
|
+
LIGHT_CYAN = "\033[1;36m"
|
|
19
|
+
LIGHT_WHITE = "\033[1;37m"
|
|
20
|
+
BOLD = "\033[1m"
|
|
21
|
+
FAINT = "\033[2m"
|
|
22
|
+
ITALIC = "\033[3m"
|
|
23
|
+
UNDERLINE = "\033[4m"
|
|
24
|
+
BLINK = "\033[5m"
|
|
25
|
+
NEGATIVE = "\033[7m"
|
|
26
|
+
CROSSED = "\033[9m"
|
|
27
|
+
END = "\033[0m"
|
|
28
|
+
reset = "\x1b[0m"
|
|
29
|
+
format = "%(message)s"
|
|
30
|
+
|
|
31
|
+
FORMATS = {
|
|
32
|
+
logging.DEBUG: CYAN + "%(levelname)s" + reset + " - " + format,
|
|
33
|
+
logging.INFO: GREEN + "%(levelname)s" + reset + " - " + format,
|
|
34
|
+
logging.WARNING: YELLOW + "%(levelname)s" + reset + " - " + format,
|
|
35
|
+
logging.ERROR: RED + "%(levelname)s" + reset + " - " + format,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Logging Configuration Function
|
|
39
|
+
def configure_logging():
|
|
40
|
+
# Get the root logger
|
|
41
|
+
logger = logging.getLogger()
|
|
42
|
+
|
|
43
|
+
# Remove all existing handlers
|
|
44
|
+
if logger.hasHandlers():
|
|
45
|
+
logger.handlers.clear()
|
|
46
|
+
|
|
47
|
+
# Initialize the handler with the custom formatter
|
|
48
|
+
handler = logging.StreamHandler()
|
|
49
|
+
handler.setFormatter(CustomFormatter())
|
|
50
|
+
|
|
51
|
+
# Set the handler for the logger
|
|
52
|
+
logger.addHandler(handler)
|
|
53
|
+
logger.setLevel(logging.WARNING)
|
|
54
|
+
|
|
55
|
+
return logger
|