flixopt 2.1.10__py3-none-any.whl → 2.2.0__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.
Potentially problematic release.
This version of flixopt might be problematic. Click here for more details.
- flixopt/__init__.py +0 -2
- flixopt/calculation.py +2 -2
- flixopt/components.py +1 -1
- flixopt/config.py +486 -193
- flixopt/elements.py +5 -5
- flixopt/features.py +6 -6
- flixopt/flow_system.py +2 -1
- flixopt/interface.py +4 -4
- flixopt/io.py +6 -4
- flixopt/results.py +1 -1
- {flixopt-2.1.10.dist-info → flixopt-2.2.0.dist-info}/METADATA +6 -8
- flixopt-2.2.0.dist-info/RECORD +25 -0
- flixopt-2.2.0.dist-info/top_level.txt +1 -0
- docs/examples/00-Minimal Example.md +0 -5
- docs/examples/01-Basic Example.md +0 -5
- docs/examples/02-Complex Example.md +0 -10
- docs/examples/03-Calculation Modes.md +0 -5
- docs/examples/index.md +0 -5
- docs/faq/contribute.md +0 -61
- docs/faq/index.md +0 -3
- docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
- docs/images/architecture_flixOpt.png +0 -0
- docs/images/flixopt-icon.svg +0 -1
- docs/javascripts/mathjax.js +0 -18
- docs/user-guide/Mathematical Notation/Bus.md +0 -33
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
- docs/user-guide/Mathematical Notation/Flow.md +0 -26
- docs/user-guide/Mathematical Notation/InvestParameters.md +0 -3
- docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
- docs/user-guide/Mathematical Notation/OnOffParameters.md +0 -3
- docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
- docs/user-guide/Mathematical Notation/Storage.md +0 -44
- docs/user-guide/Mathematical Notation/index.md +0 -22
- docs/user-guide/Mathematical Notation/others.md +0 -3
- docs/user-guide/index.md +0 -124
- flixopt/config.yaml +0 -10
- flixopt-2.1.10.dist-info/RECORD +0 -57
- flixopt-2.1.10.dist-info/top_level.txt +0 -5
- pics/architecture_flixOpt-pre2.0.0.png +0 -0
- pics/architecture_flixOpt.png +0 -0
- pics/flixOpt_plotting.jpg +0 -0
- pics/flixopt-icon.svg +0 -1
- pics/pics.pptx +0 -0
- scripts/extract_changelog.py +0 -148
- scripts/extract_release_notes.py +0 -45
- scripts/gen_ref_pages.py +0 -54
- tests/ressources/Zeitreihen2020.csv +0 -35137
- {flixopt-2.1.10.dist-info → flixopt-2.2.0.dist-info}/WHEEL +0 -0
- {flixopt-2.1.10.dist-info → flixopt-2.2.0.dist-info}/licenses/LICENSE +0 -0
flixopt/config.py
CHANGED
|
@@ -1,168 +1,345 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
from
|
|
7
|
-
from
|
|
4
|
+
import warnings
|
|
5
|
+
from logging.handlers import RotatingFileHandler
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import MappingProxyType
|
|
8
|
+
from typing import Literal
|
|
8
9
|
|
|
9
10
|
import yaml
|
|
10
11
|
from rich.console import Console
|
|
11
12
|
from rich.logging import RichHandler
|
|
13
|
+
from rich.style import Style
|
|
14
|
+
from rich.theme import Theme
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def merge_configs(defaults: dict, overrides: dict) -> dict:
|
|
17
|
-
"""
|
|
18
|
-
Merge the default configuration with user-provided overrides.
|
|
19
|
-
Args:
|
|
20
|
-
defaults: Default configuration dictionary.
|
|
21
|
-
overrides: User configuration dictionary.
|
|
22
|
-
Returns:
|
|
23
|
-
Merged configuration dictionary.
|
|
24
|
-
"""
|
|
25
|
-
for key, value in overrides.items():
|
|
26
|
-
if isinstance(value, dict) and key in defaults and isinstance(defaults[key], dict):
|
|
27
|
-
# Recursively merge nested dictionaries
|
|
28
|
-
defaults[key] = merge_configs(defaults[key], value)
|
|
29
|
-
else:
|
|
30
|
-
# Override the default value
|
|
31
|
-
defaults[key] = value
|
|
32
|
-
return defaults
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def dataclass_from_dict_with_validation(cls, data: dict):
|
|
36
|
-
"""
|
|
37
|
-
Recursively initialize a dataclass from a dictionary.
|
|
38
|
-
"""
|
|
39
|
-
if not is_dataclass(cls):
|
|
40
|
-
raise TypeError(f'{cls} must be a dataclass')
|
|
41
|
-
|
|
42
|
-
# Get resolved type hints to handle postponed evaluation
|
|
43
|
-
type_hints = get_type_hints(cls)
|
|
44
|
-
|
|
45
|
-
# Build kwargs for the dataclass constructor
|
|
46
|
-
kwargs = {}
|
|
47
|
-
for field in fields(cls):
|
|
48
|
-
field_name = field.name
|
|
49
|
-
# Use resolved type from get_type_hints instead of field.type
|
|
50
|
-
field_type = type_hints.get(field_name, field.type)
|
|
51
|
-
field_value = data.get(field_name)
|
|
52
|
-
|
|
53
|
-
# If the field type is a dataclass and the value is a dict, recursively initialize
|
|
54
|
-
if is_dataclass(field_type) and isinstance(field_value, dict):
|
|
55
|
-
kwargs[field_name] = dataclass_from_dict_with_validation(field_type, field_value)
|
|
56
|
-
else:
|
|
57
|
-
kwargs[field_name] = field_value # Pass as-is if no special handling is needed
|
|
58
|
-
|
|
59
|
-
return cls(**kwargs)
|
|
16
|
+
__all__ = ['CONFIG', 'change_logging_level']
|
|
60
17
|
|
|
18
|
+
logger = logging.getLogger('flixopt')
|
|
61
19
|
|
|
62
|
-
@dataclass()
|
|
63
|
-
class ValidatedConfig:
|
|
64
|
-
def __setattr__(self, name, value):
|
|
65
|
-
if field := self.__dataclass_fields__.get(name):
|
|
66
|
-
# Get resolved type hints to handle postponed evaluation
|
|
67
|
-
type_hints = get_type_hints(self.__class__, include_extras=True)
|
|
68
|
-
field_type = type_hints.get(name, field.type)
|
|
69
|
-
if metadata := getattr(field_type, '__metadata__', None):
|
|
70
|
-
assert metadata[0](value), f'Invalid value passed to {name!r}: {value=}'
|
|
71
|
-
super().__setattr__(name, value)
|
|
72
20
|
|
|
21
|
+
# SINGLE SOURCE OF TRUTH - immutable to prevent accidental modification
|
|
22
|
+
_DEFAULTS = MappingProxyType(
|
|
23
|
+
{
|
|
24
|
+
'config_name': 'flixopt',
|
|
25
|
+
'logging': MappingProxyType(
|
|
26
|
+
{
|
|
27
|
+
'level': 'INFO',
|
|
28
|
+
'file': 'flixopt.log',
|
|
29
|
+
'rich': False,
|
|
30
|
+
'console': True,
|
|
31
|
+
'max_file_size': 10_485_760, # 10MB
|
|
32
|
+
'backup_count': 5,
|
|
33
|
+
'date_format': '%Y-%m-%d %H:%M:%S',
|
|
34
|
+
'format': '%(message)s',
|
|
35
|
+
'console_width': 120,
|
|
36
|
+
'show_path': False,
|
|
37
|
+
'colors': MappingProxyType(
|
|
38
|
+
{
|
|
39
|
+
'DEBUG': '\033[32m', # Green
|
|
40
|
+
'INFO': '\033[34m', # Blue
|
|
41
|
+
'WARNING': '\033[33m', # Yellow
|
|
42
|
+
'ERROR': '\033[31m', # Red
|
|
43
|
+
'CRITICAL': '\033[1m\033[31m', # Bold Red
|
|
44
|
+
}
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
),
|
|
48
|
+
'modeling': MappingProxyType(
|
|
49
|
+
{
|
|
50
|
+
'big': 10_000_000,
|
|
51
|
+
'epsilon': 1e-5,
|
|
52
|
+
'big_binary_bound': 100_000,
|
|
53
|
+
}
|
|
54
|
+
),
|
|
55
|
+
}
|
|
56
|
+
)
|
|
73
57
|
|
|
74
|
-
@dataclass
|
|
75
|
-
class LoggingConfig(ValidatedConfig):
|
|
76
|
-
level: Annotated[
|
|
77
|
-
Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
|
78
|
-
lambda level: level in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
|
79
|
-
]
|
|
80
|
-
file: Annotated[str, lambda file: isinstance(file, str)]
|
|
81
|
-
rich: Annotated[bool, lambda rich: isinstance(rich, bool)]
|
|
82
58
|
|
|
59
|
+
class CONFIG:
|
|
60
|
+
"""Configuration for flixopt library.
|
|
61
|
+
|
|
62
|
+
The CONFIG class provides centralized configuration for logging and modeling parameters.
|
|
63
|
+
All changes require calling ``CONFIG.apply()`` to take effect.
|
|
64
|
+
|
|
65
|
+
By default, logging outputs to both console and file ('flixopt.log').
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
Logging: Nested class containing all logging configuration options.
|
|
69
|
+
Colors: Nested subclass under Logging containing ANSI color codes for log levels.
|
|
70
|
+
Modeling: Nested class containing optimization modeling parameters.
|
|
71
|
+
config_name (str): Name of the configuration (default: 'flixopt').
|
|
72
|
+
|
|
73
|
+
Logging Attributes:
|
|
74
|
+
level (str): Logging level: 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'.
|
|
75
|
+
Default: 'INFO'
|
|
76
|
+
file (str | None): Log file path. Default: 'flixopt.log'.
|
|
77
|
+
Set to None to disable file logging.
|
|
78
|
+
console (bool): Enable console (stdout) logging. Default: True
|
|
79
|
+
rich (bool): Use Rich library for enhanced console output. Default: False
|
|
80
|
+
max_file_size (int): Maximum log file size in bytes before rotation.
|
|
81
|
+
Default: 10485760 (10MB)
|
|
82
|
+
backup_count (int): Number of backup log files to keep. Default: 5
|
|
83
|
+
date_format (str): Date/time format for log messages.
|
|
84
|
+
Default: '%Y-%m-%d %H:%M:%S'
|
|
85
|
+
format (str): Log message format string. Default: '%(message)s'
|
|
86
|
+
console_width (int): Console width for Rich handler. Default: 120
|
|
87
|
+
show_path (bool): Show file paths in log messages. Default: False
|
|
88
|
+
|
|
89
|
+
Colors Attributes:
|
|
90
|
+
DEBUG (str): ANSI color code for DEBUG level. Default: '\\033[32m' (green)
|
|
91
|
+
INFO (str): ANSI color code for INFO level. Default: '\\033[34m' (blue)
|
|
92
|
+
WARNING (str): ANSI color code for WARNING level. Default: '\\033[33m' (yellow)
|
|
93
|
+
ERROR (str): ANSI color code for ERROR level. Default: '\\033[31m' (red)
|
|
94
|
+
CRITICAL (str): ANSI color code for CRITICAL level. Default: '\\033[1m\\033[31m' (bold red)
|
|
95
|
+
|
|
96
|
+
Works with both Rich and standard console handlers.
|
|
97
|
+
Rich automatically converts ANSI codes using Style.from_ansi().
|
|
98
|
+
|
|
99
|
+
Common ANSI codes:
|
|
100
|
+
|
|
101
|
+
- '\\033[30m' - Black
|
|
102
|
+
- '\\033[31m' - Red
|
|
103
|
+
- '\\033[32m' - Green
|
|
104
|
+
- '\\033[33m' - Yellow
|
|
105
|
+
- '\\033[34m' - Blue
|
|
106
|
+
- '\\033[35m' - Magenta
|
|
107
|
+
- '\\033[36m' - Cyan
|
|
108
|
+
- '\\033[37m' - White
|
|
109
|
+
- '\\033[1m\\033[3Xm' - Bold color (replace X with color code 0-7)
|
|
110
|
+
- '\\033[2m\\033[3Xm' - Dim color (replace X with color code 0-7)
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
|
|
114
|
+
- Magenta: '\\033[35m'
|
|
115
|
+
- Bold cyan: '\\033[1m\\033[36m'
|
|
116
|
+
- Dim green: '\\033[2m\\033[32m'
|
|
117
|
+
|
|
118
|
+
Modeling Attributes:
|
|
119
|
+
big (int): Large number for optimization constraints. Default: 10000000
|
|
120
|
+
epsilon (float): Small tolerance value. Default: 1e-5
|
|
121
|
+
big_binary_bound (int): Upper bound for binary variable constraints.
|
|
122
|
+
Default: 100000
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
Basic configuration::
|
|
126
|
+
|
|
127
|
+
from flixopt import CONFIG
|
|
128
|
+
|
|
129
|
+
CONFIG.Logging.console = True
|
|
130
|
+
CONFIG.Logging.level = 'DEBUG'
|
|
131
|
+
CONFIG.apply()
|
|
132
|
+
|
|
133
|
+
Configure log file rotation::
|
|
134
|
+
|
|
135
|
+
CONFIG.Logging.file = 'myapp.log'
|
|
136
|
+
CONFIG.Logging.max_file_size = 5_242_880 # 5 MB
|
|
137
|
+
CONFIG.Logging.backup_count = 3
|
|
138
|
+
CONFIG.apply()
|
|
139
|
+
|
|
140
|
+
Customize log colors::
|
|
141
|
+
|
|
142
|
+
CONFIG.Logging.Colors.INFO = '\\033[35m' # Magenta
|
|
143
|
+
CONFIG.Logging.Colors.DEBUG = '\\033[36m' # Cyan
|
|
144
|
+
CONFIG.Logging.Colors.ERROR = '\\033[1m\\033[31m' # Bold red
|
|
145
|
+
CONFIG.apply()
|
|
146
|
+
|
|
147
|
+
Use Rich handler with custom colors::
|
|
148
|
+
|
|
149
|
+
CONFIG.Logging.console = True
|
|
150
|
+
CONFIG.Logging.rich = True
|
|
151
|
+
CONFIG.Logging.console_width = 100
|
|
152
|
+
CONFIG.Logging.show_path = True
|
|
153
|
+
CONFIG.Logging.Colors.INFO = '\\033[36m' # Cyan
|
|
154
|
+
CONFIG.apply()
|
|
155
|
+
|
|
156
|
+
Load from YAML file::
|
|
157
|
+
|
|
158
|
+
CONFIG.load_from_file('config.yaml')
|
|
159
|
+
|
|
160
|
+
Example YAML config file:
|
|
161
|
+
|
|
162
|
+
.. code-block:: yaml
|
|
163
|
+
|
|
164
|
+
logging:
|
|
165
|
+
level: DEBUG
|
|
166
|
+
console: true
|
|
167
|
+
file: app.log
|
|
168
|
+
rich: true
|
|
169
|
+
max_file_size: 5242880 # 5MB
|
|
170
|
+
backup_count: 3
|
|
171
|
+
date_format: '%H:%M:%S'
|
|
172
|
+
console_width: 100
|
|
173
|
+
show_path: true
|
|
174
|
+
colors:
|
|
175
|
+
DEBUG: "\\033[36m" # Cyan
|
|
176
|
+
INFO: "\\033[32m" # Green
|
|
177
|
+
WARNING: "\\033[33m" # Yellow
|
|
178
|
+
ERROR: "\\033[31m" # Red
|
|
179
|
+
CRITICAL: "\\033[1m\\033[31m" # Bold red
|
|
180
|
+
|
|
181
|
+
modeling:
|
|
182
|
+
big: 20000000
|
|
183
|
+
epsilon: 1e-6
|
|
184
|
+
big_binary_bound: 200000
|
|
185
|
+
|
|
186
|
+
Reset to defaults::
|
|
83
187
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
EPSILON: Annotated[float, lambda x: isinstance(x, float)]
|
|
88
|
-
BIG_BINARY_BOUND: Annotated[int, lambda x: isinstance(x, int)]
|
|
188
|
+
CONFIG.reset()
|
|
189
|
+
|
|
190
|
+
Export current configuration::
|
|
89
191
|
|
|
192
|
+
config_dict = CONFIG.to_dict()
|
|
193
|
+
import yaml
|
|
90
194
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
logging: LoggingConfig
|
|
95
|
-
modeling: ModelingConfig
|
|
195
|
+
with open('my_config.yaml', 'w') as f:
|
|
196
|
+
yaml.dump(config_dict, f)
|
|
197
|
+
"""
|
|
96
198
|
|
|
199
|
+
class Logging:
|
|
200
|
+
level: str = _DEFAULTS['logging']['level']
|
|
201
|
+
file: str | None = _DEFAULTS['logging']['file']
|
|
202
|
+
rich: bool = _DEFAULTS['logging']['rich']
|
|
203
|
+
console: bool = _DEFAULTS['logging']['console']
|
|
204
|
+
max_file_size: int = _DEFAULTS['logging']['max_file_size']
|
|
205
|
+
backup_count: int = _DEFAULTS['logging']['backup_count']
|
|
206
|
+
date_format: str = _DEFAULTS['logging']['date_format']
|
|
207
|
+
format: str = _DEFAULTS['logging']['format']
|
|
208
|
+
console_width: int = _DEFAULTS['logging']['console_width']
|
|
209
|
+
show_path: bool = _DEFAULTS['logging']['show_path']
|
|
210
|
+
|
|
211
|
+
class Colors:
|
|
212
|
+
DEBUG: str = _DEFAULTS['logging']['colors']['DEBUG']
|
|
213
|
+
INFO: str = _DEFAULTS['logging']['colors']['INFO']
|
|
214
|
+
WARNING: str = _DEFAULTS['logging']['colors']['WARNING']
|
|
215
|
+
ERROR: str = _DEFAULTS['logging']['colors']['ERROR']
|
|
216
|
+
CRITICAL: str = _DEFAULTS['logging']['colors']['CRITICAL']
|
|
217
|
+
|
|
218
|
+
class Modeling:
|
|
219
|
+
big: int = _DEFAULTS['modeling']['big']
|
|
220
|
+
epsilon: float = _DEFAULTS['modeling']['epsilon']
|
|
221
|
+
big_binary_bound: int = _DEFAULTS['modeling']['big_binary_bound']
|
|
222
|
+
|
|
223
|
+
config_name: str = _DEFAULTS['config_name']
|
|
97
224
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
225
|
+
@classmethod
|
|
226
|
+
def reset(cls):
|
|
227
|
+
"""Reset all configuration values to defaults."""
|
|
228
|
+
for key, value in _DEFAULTS['logging'].items():
|
|
229
|
+
if key == 'colors':
|
|
230
|
+
# Reset nested Colors class
|
|
231
|
+
for color_key, color_value in value.items():
|
|
232
|
+
setattr(cls.Logging.Colors, color_key, color_value)
|
|
233
|
+
else:
|
|
234
|
+
setattr(cls.Logging, key, value)
|
|
235
|
+
|
|
236
|
+
for key, value in _DEFAULTS['modeling'].items():
|
|
237
|
+
setattr(cls.Modeling, key, value)
|
|
238
|
+
|
|
239
|
+
cls.config_name = _DEFAULTS['config_name']
|
|
240
|
+
cls.apply()
|
|
102
241
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
242
|
+
@classmethod
|
|
243
|
+
def apply(cls):
|
|
244
|
+
"""Apply current configuration to logging system."""
|
|
245
|
+
# Convert Colors class attributes to dict
|
|
246
|
+
colors_dict = {
|
|
247
|
+
'DEBUG': cls.Logging.Colors.DEBUG,
|
|
248
|
+
'INFO': cls.Logging.Colors.INFO,
|
|
249
|
+
'WARNING': cls.Logging.Colors.WARNING,
|
|
250
|
+
'ERROR': cls.Logging.Colors.ERROR,
|
|
251
|
+
'CRITICAL': cls.Logging.Colors.CRITICAL,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_setup_logging(
|
|
255
|
+
default_level=cls.Logging.level,
|
|
256
|
+
log_file=cls.Logging.file,
|
|
257
|
+
use_rich_handler=cls.Logging.rich,
|
|
258
|
+
console=cls.Logging.console,
|
|
259
|
+
max_file_size=cls.Logging.max_file_size,
|
|
260
|
+
backup_count=cls.Logging.backup_count,
|
|
261
|
+
date_format=cls.Logging.date_format,
|
|
262
|
+
format=cls.Logging.format,
|
|
263
|
+
console_width=cls.Logging.console_width,
|
|
264
|
+
show_path=cls.Logging.show_path,
|
|
265
|
+
colors=colors_dict,
|
|
266
|
+
)
|
|
106
267
|
|
|
107
268
|
@classmethod
|
|
108
|
-
def
|
|
109
|
-
"""
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
default_config_path = os.path.join(os.path.dirname(__file__), 'config.yaml')
|
|
114
|
-
|
|
115
|
-
if user_config_file is None:
|
|
116
|
-
with open(default_config_path) as file:
|
|
117
|
-
new_config = yaml.safe_load(file)
|
|
118
|
-
elif not os.path.exists(user_config_file):
|
|
119
|
-
raise FileNotFoundError(f'Config file not found: {user_config_file}')
|
|
120
|
-
else:
|
|
121
|
-
with open(user_config_file) as user_file:
|
|
122
|
-
new_config = yaml.safe_load(user_file)
|
|
269
|
+
def load_from_file(cls, config_file: str | Path):
|
|
270
|
+
"""Load configuration from YAML file and apply it."""
|
|
271
|
+
config_path = Path(config_file)
|
|
272
|
+
if not config_path.exists():
|
|
273
|
+
raise FileNotFoundError(f'Config file not found: {config_file}')
|
|
123
274
|
|
|
124
|
-
|
|
125
|
-
|
|
275
|
+
with config_path.open() as file:
|
|
276
|
+
config_dict = yaml.safe_load(file)
|
|
277
|
+
cls._apply_config_dict(config_dict)
|
|
126
278
|
|
|
127
|
-
|
|
128
|
-
cls.logging = config_data.logging
|
|
129
|
-
cls.modeling = config_data.modeling
|
|
130
|
-
cls.config_name = config_data.config_name
|
|
279
|
+
cls.apply()
|
|
131
280
|
|
|
132
|
-
|
|
281
|
+
@classmethod
|
|
282
|
+
def _apply_config_dict(cls, config_dict: dict):
|
|
283
|
+
"""Apply configuration dictionary to class attributes."""
|
|
284
|
+
for key, value in config_dict.items():
|
|
285
|
+
if key == 'logging' and isinstance(value, dict):
|
|
286
|
+
for nested_key, nested_value in value.items():
|
|
287
|
+
if nested_key == 'colors' and isinstance(nested_value, dict):
|
|
288
|
+
# Handle nested colors under logging
|
|
289
|
+
for color_key, color_value in nested_value.items():
|
|
290
|
+
setattr(cls.Logging.Colors, color_key, color_value)
|
|
291
|
+
else:
|
|
292
|
+
setattr(cls.Logging, nested_key, nested_value)
|
|
293
|
+
elif key == 'modeling' and isinstance(value, dict):
|
|
294
|
+
for nested_key, nested_value in value.items():
|
|
295
|
+
setattr(cls.Modeling, nested_key, nested_value)
|
|
296
|
+
elif hasattr(cls, key):
|
|
297
|
+
setattr(cls, key, value)
|
|
133
298
|
|
|
134
299
|
@classmethod
|
|
135
300
|
def to_dict(cls):
|
|
136
|
-
"""
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
301
|
+
"""Convert the configuration class into a dictionary for JSON serialization."""
|
|
302
|
+
return {
|
|
303
|
+
'config_name': cls.config_name,
|
|
304
|
+
'logging': {
|
|
305
|
+
'level': cls.Logging.level,
|
|
306
|
+
'file': cls.Logging.file,
|
|
307
|
+
'rich': cls.Logging.rich,
|
|
308
|
+
'console': cls.Logging.console,
|
|
309
|
+
'max_file_size': cls.Logging.max_file_size,
|
|
310
|
+
'backup_count': cls.Logging.backup_count,
|
|
311
|
+
'date_format': cls.Logging.date_format,
|
|
312
|
+
'format': cls.Logging.format,
|
|
313
|
+
'console_width': cls.Logging.console_width,
|
|
314
|
+
'show_path': cls.Logging.show_path,
|
|
315
|
+
'colors': {
|
|
316
|
+
'DEBUG': cls.Logging.Colors.DEBUG,
|
|
317
|
+
'INFO': cls.Logging.Colors.INFO,
|
|
318
|
+
'WARNING': cls.Logging.Colors.WARNING,
|
|
319
|
+
'ERROR': cls.Logging.Colors.ERROR,
|
|
320
|
+
'CRITICAL': cls.Logging.Colors.CRITICAL,
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
'modeling': {
|
|
324
|
+
'big': cls.Modeling.big,
|
|
325
|
+
'epsilon': cls.Modeling.epsilon,
|
|
326
|
+
'big_binary_bound': cls.Modeling.big_binary_bound,
|
|
327
|
+
},
|
|
328
|
+
}
|
|
154
329
|
|
|
155
330
|
|
|
156
331
|
class MultilineFormater(logging.Formatter):
|
|
332
|
+
"""Formatter that handles multi-line messages with consistent prefixes."""
|
|
333
|
+
|
|
334
|
+
def __init__(self, fmt=None, datefmt=None):
|
|
335
|
+
super().__init__(fmt=fmt, datefmt=datefmt)
|
|
336
|
+
|
|
157
337
|
def format(self, record):
|
|
158
338
|
message_lines = record.getMessage().split('\n')
|
|
159
|
-
|
|
160
|
-
# Prepare the log prefix (timestamp + log level)
|
|
161
339
|
timestamp = self.formatTime(record, self.datefmt)
|
|
162
|
-
log_level = record.levelname.ljust(8)
|
|
340
|
+
log_level = record.levelname.ljust(8)
|
|
163
341
|
log_prefix = f'{timestamp} | {log_level} |'
|
|
164
342
|
|
|
165
|
-
# Format all lines
|
|
166
343
|
first_line = [f'{log_prefix} {message_lines[0]}']
|
|
167
344
|
if len(message_lines) > 1:
|
|
168
345
|
lines = first_line + [f'{log_prefix} {line}' for line in message_lines[1:]]
|
|
@@ -173,96 +350,212 @@ class MultilineFormater(logging.Formatter):
|
|
|
173
350
|
|
|
174
351
|
|
|
175
352
|
class ColoredMultilineFormater(MultilineFormater):
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
'DEBUG': '\033[32m', # Green
|
|
179
|
-
'INFO': '\033[34m', # Blue
|
|
180
|
-
'WARNING': '\033[33m', # Yellow
|
|
181
|
-
'ERROR': '\033[31m', # Red
|
|
182
|
-
'CRITICAL': '\033[1m\033[31m', # Bold Red
|
|
183
|
-
}
|
|
353
|
+
"""Formatter that adds ANSI colors to multi-line log messages."""
|
|
354
|
+
|
|
184
355
|
RESET = '\033[0m'
|
|
185
356
|
|
|
357
|
+
def __init__(self, fmt=None, datefmt=None, colors=None):
|
|
358
|
+
super().__init__(fmt=fmt, datefmt=datefmt)
|
|
359
|
+
self.COLORS = (
|
|
360
|
+
colors
|
|
361
|
+
if colors is not None
|
|
362
|
+
else {
|
|
363
|
+
'DEBUG': '\033[32m',
|
|
364
|
+
'INFO': '\033[34m',
|
|
365
|
+
'WARNING': '\033[33m',
|
|
366
|
+
'ERROR': '\033[31m',
|
|
367
|
+
'CRITICAL': '\033[1m\033[31m',
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
|
|
186
371
|
def format(self, record):
|
|
187
372
|
lines = super().format(record).splitlines()
|
|
188
373
|
log_color = self.COLORS.get(record.levelname, self.RESET)
|
|
374
|
+
formatted_lines = [f'{log_color}{line}{self.RESET}' for line in lines]
|
|
375
|
+
return '\n'.join(formatted_lines)
|
|
189
376
|
|
|
190
|
-
# Create a formatted message for each line separately
|
|
191
|
-
formatted_lines = []
|
|
192
|
-
for line in lines:
|
|
193
|
-
formatted_lines.append(f'{log_color}{line}{self.RESET}')
|
|
194
377
|
|
|
195
|
-
|
|
378
|
+
def _create_console_handler(
|
|
379
|
+
use_rich: bool = False,
|
|
380
|
+
console_width: int = 120,
|
|
381
|
+
show_path: bool = False,
|
|
382
|
+
date_format: str = '%Y-%m-%d %H:%M:%S',
|
|
383
|
+
format: str = '%(message)s',
|
|
384
|
+
colors: dict[str, str] | None = None,
|
|
385
|
+
) -> logging.Handler:
|
|
386
|
+
"""Create a console (stdout) logging handler.
|
|
196
387
|
|
|
388
|
+
Args:
|
|
389
|
+
use_rich: If True, use RichHandler with color support.
|
|
390
|
+
console_width: Width of the console for Rich handler.
|
|
391
|
+
show_path: Show file paths in log messages (Rich only).
|
|
392
|
+
date_format: Date/time format string.
|
|
393
|
+
format: Log message format string.
|
|
394
|
+
colors: Dictionary of ANSI color codes for each log level.
|
|
197
395
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
396
|
+
Returns:
|
|
397
|
+
Configured logging handler (RichHandler or StreamHandler).
|
|
398
|
+
"""
|
|
399
|
+
if use_rich:
|
|
400
|
+
# Convert ANSI codes to Rich theme
|
|
401
|
+
if colors:
|
|
402
|
+
theme_dict = {}
|
|
403
|
+
for level, ansi_code in colors.items():
|
|
404
|
+
# Rich can parse ANSI codes directly!
|
|
405
|
+
try:
|
|
406
|
+
style = Style.from_ansi(ansi_code)
|
|
407
|
+
theme_dict[f'logging.level.{level.lower()}'] = style
|
|
408
|
+
except Exception:
|
|
409
|
+
# Fallback to default if parsing fails
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
theme = Theme(theme_dict) if theme_dict else None
|
|
413
|
+
else:
|
|
414
|
+
theme = None
|
|
415
|
+
|
|
416
|
+
console = Console(width=console_width, theme=theme)
|
|
417
|
+
handler = RichHandler(
|
|
204
418
|
console=console,
|
|
205
419
|
rich_tracebacks=True,
|
|
206
420
|
omit_repeated_times=True,
|
|
207
|
-
show_path=
|
|
208
|
-
log_time_format=
|
|
209
|
-
)
|
|
210
|
-
rich_handler.setFormatter(logging.Formatter('%(message)s')) # Simplified formatting
|
|
211
|
-
|
|
212
|
-
return rich_handler
|
|
213
|
-
elif log_file is None:
|
|
214
|
-
# Regular Logger with custom formating enabled
|
|
215
|
-
file_handler = logging.StreamHandler()
|
|
216
|
-
file_handler.setFormatter(
|
|
217
|
-
ColoredMultilineFormater(
|
|
218
|
-
fmt='%(message)s',
|
|
219
|
-
datefmt='%Y-%m-%d %H:%M:%S',
|
|
220
|
-
)
|
|
421
|
+
show_path=show_path,
|
|
422
|
+
log_time_format=date_format,
|
|
221
423
|
)
|
|
222
|
-
|
|
424
|
+
handler.setFormatter(logging.Formatter(format))
|
|
223
425
|
else:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
426
|
+
handler = logging.StreamHandler()
|
|
427
|
+
handler.setFormatter(ColoredMultilineFormater(fmt=format, datefmt=date_format, colors=colors))
|
|
428
|
+
|
|
429
|
+
return handler
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _create_file_handler(
|
|
433
|
+
log_file: str,
|
|
434
|
+
max_file_size: int = 10_485_760,
|
|
435
|
+
backup_count: int = 5,
|
|
436
|
+
date_format: str = '%Y-%m-%d %H:%M:%S',
|
|
437
|
+
format: str = '%(message)s',
|
|
438
|
+
) -> RotatingFileHandler:
|
|
439
|
+
"""Create a rotating file handler to prevent huge log files.
|
|
233
440
|
|
|
441
|
+
Args:
|
|
442
|
+
log_file: Path to the log file.
|
|
443
|
+
max_file_size: Maximum size in bytes before rotation.
|
|
444
|
+
backup_count: Number of backup files to keep.
|
|
445
|
+
date_format: Date/time format string.
|
|
446
|
+
format: Log message format string.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Configured RotatingFileHandler (without colors).
|
|
450
|
+
"""
|
|
451
|
+
handler = RotatingFileHandler(
|
|
452
|
+
log_file,
|
|
453
|
+
maxBytes=max_file_size,
|
|
454
|
+
backupCount=backup_count,
|
|
455
|
+
encoding='utf-8',
|
|
456
|
+
)
|
|
457
|
+
handler.setFormatter(MultilineFormater(fmt=format, datefmt=date_format))
|
|
458
|
+
return handler
|
|
234
459
|
|
|
235
|
-
|
|
460
|
+
|
|
461
|
+
def _setup_logging(
|
|
236
462
|
default_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO',
|
|
237
|
-
log_file: str | None =
|
|
463
|
+
log_file: str | None = None,
|
|
238
464
|
use_rich_handler: bool = False,
|
|
465
|
+
console: bool = False,
|
|
466
|
+
max_file_size: int = 10_485_760,
|
|
467
|
+
backup_count: int = 5,
|
|
468
|
+
date_format: str = '%Y-%m-%d %H:%M:%S',
|
|
469
|
+
format: str = '%(message)s',
|
|
470
|
+
console_width: int = 120,
|
|
471
|
+
show_path: bool = False,
|
|
472
|
+
colors: dict[str, str] | None = None,
|
|
239
473
|
):
|
|
240
|
-
"""
|
|
241
|
-
logger = logging.getLogger('flixopt') # Use a specific logger name for your package
|
|
242
|
-
logger.setLevel(get_logging_level_by_name(default_level))
|
|
243
|
-
# Clear existing handlers
|
|
244
|
-
if logger.hasHandlers():
|
|
245
|
-
logger.handlers.clear()
|
|
474
|
+
"""Internal function to setup logging - use CONFIG.apply() instead.
|
|
246
475
|
|
|
247
|
-
logger.
|
|
248
|
-
|
|
249
|
-
logger.addHandler(_get_logging_handler(log_file, use_rich_handler=False))
|
|
476
|
+
Configures the flixopt logger with console and/or file handlers.
|
|
477
|
+
If no handlers are configured, adds NullHandler (library best practice).
|
|
250
478
|
|
|
251
|
-
|
|
479
|
+
Args:
|
|
480
|
+
default_level: Logging level for the logger.
|
|
481
|
+
log_file: Path to log file (None to disable file logging).
|
|
482
|
+
use_rich_handler: Use Rich for enhanced console output.
|
|
483
|
+
console: Enable console logging.
|
|
484
|
+
max_file_size: Maximum log file size before rotation.
|
|
485
|
+
backup_count: Number of backup log files to keep.
|
|
486
|
+
date_format: Date/time format for log messages.
|
|
487
|
+
format: Log message format string.
|
|
488
|
+
console_width: Console width for Rich handler.
|
|
489
|
+
show_path: Show file paths in log messages (Rich only).
|
|
490
|
+
colors: ANSI color codes for each log level.
|
|
491
|
+
"""
|
|
492
|
+
logger = logging.getLogger('flixopt')
|
|
493
|
+
logger.setLevel(getattr(logging, default_level.upper()))
|
|
494
|
+
logger.propagate = False # Prevent duplicate logs
|
|
495
|
+
logger.handlers.clear()
|
|
496
|
+
|
|
497
|
+
if console:
|
|
498
|
+
logger.addHandler(
|
|
499
|
+
_create_console_handler(
|
|
500
|
+
use_rich=use_rich_handler,
|
|
501
|
+
console_width=console_width,
|
|
502
|
+
show_path=show_path,
|
|
503
|
+
date_format=date_format,
|
|
504
|
+
format=format,
|
|
505
|
+
colors=colors,
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
if log_file:
|
|
510
|
+
logger.addHandler(
|
|
511
|
+
_create_file_handler(
|
|
512
|
+
log_file=log_file,
|
|
513
|
+
max_file_size=max_file_size,
|
|
514
|
+
backup_count=backup_count,
|
|
515
|
+
date_format=date_format,
|
|
516
|
+
format=format,
|
|
517
|
+
)
|
|
518
|
+
)
|
|
252
519
|
|
|
520
|
+
# Library best practice: NullHandler if no handlers configured
|
|
521
|
+
if not logger.handlers:
|
|
522
|
+
logger.addHandler(logging.NullHandler())
|
|
253
523
|
|
|
254
|
-
|
|
255
|
-
possible_logging_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
|
256
|
-
if level_name.upper() not in possible_logging_levels:
|
|
257
|
-
raise ValueError(f'Invalid logging level {level_name}')
|
|
258
|
-
else:
|
|
259
|
-
logging_level = getattr(logging, level_name.upper(), logging.WARNING)
|
|
260
|
-
return logging_level
|
|
524
|
+
return logger
|
|
261
525
|
|
|
262
526
|
|
|
263
527
|
def change_logging_level(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']):
|
|
528
|
+
"""
|
|
529
|
+
Change the logging level for the flixopt logger and all its handlers.
|
|
530
|
+
|
|
531
|
+
.. deprecated:: 2.1.11
|
|
532
|
+
Use ``CONFIG.Logging.level = level_name`` and ``CONFIG.apply()`` instead.
|
|
533
|
+
This function will be removed in version 3.0.0.
|
|
534
|
+
|
|
535
|
+
Parameters
|
|
536
|
+
----------
|
|
537
|
+
level_name : {'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'}
|
|
538
|
+
The logging level to set.
|
|
539
|
+
|
|
540
|
+
Examples
|
|
541
|
+
--------
|
|
542
|
+
>>> change_logging_level('DEBUG') # deprecated
|
|
543
|
+
>>> # Use this instead:
|
|
544
|
+
>>> CONFIG.Logging.level = 'DEBUG'
|
|
545
|
+
>>> CONFIG.apply()
|
|
546
|
+
"""
|
|
547
|
+
warnings.warn(
|
|
548
|
+
'change_logging_level is deprecated and will be removed in version 3.0.0. '
|
|
549
|
+
'Use CONFIG.Logging.level = level_name and CONFIG.apply() instead.',
|
|
550
|
+
DeprecationWarning,
|
|
551
|
+
stacklevel=2,
|
|
552
|
+
)
|
|
264
553
|
logger = logging.getLogger('flixopt')
|
|
265
|
-
logging_level =
|
|
554
|
+
logging_level = getattr(logging, level_name.upper())
|
|
266
555
|
logger.setLevel(logging_level)
|
|
267
556
|
for handler in logger.handlers:
|
|
268
557
|
handler.setLevel(logging_level)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# Initialize default config
|
|
561
|
+
CONFIG.apply()
|