flixopt 2.2.0rc2__py3-none-any.whl → 3.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.
- flixopt/__init__.py +33 -4
- flixopt/aggregation.py +60 -80
- flixopt/calculation.py +403 -182
- flixopt/commons.py +1 -10
- flixopt/components.py +939 -448
- flixopt/config.py +553 -191
- flixopt/core.py +513 -846
- flixopt/effects.py +644 -178
- flixopt/elements.py +610 -355
- flixopt/features.py +394 -966
- flixopt/flow_system.py +736 -219
- flixopt/interface.py +1104 -302
- flixopt/io.py +103 -79
- flixopt/linear_converters.py +387 -95
- flixopt/modeling.py +757 -0
- flixopt/network_app.py +73 -39
- flixopt/plotting.py +294 -138
- flixopt/results.py +1254 -300
- flixopt/solvers.py +25 -21
- flixopt/structure.py +938 -396
- flixopt/utils.py +36 -12
- flixopt-3.0.1.dist-info/METADATA +209 -0
- flixopt-3.0.1.dist-info/RECORD +26 -0
- flixopt-3.0.1.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/LinearConverter.md +0 -21
- 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.2.0rc2.dist-info/METADATA +0 -167
- flixopt-2.2.0rc2.dist-info/RECORD +0 -54
- flixopt-2.2.0rc2.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_release_notes.py +0 -45
- scripts/gen_ref_pages.py +0 -54
- tests/ressources/Zeitreihen2020.csv +0 -35137
- {flixopt-2.2.0rc2.dist-info → flixopt-3.0.1.dist-info}/WHEEL +0 -0
- {flixopt-2.2.0rc2.dist-info → flixopt-3.0.1.dist-info}/licenses/LICENSE +0 -0
flixopt/config.py
CHANGED
|
@@ -1,259 +1,621 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
from
|
|
5
|
-
from
|
|
4
|
+
import sys
|
|
5
|
+
import warnings
|
|
6
|
+
from logging.handlers import RotatingFileHandler
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Literal
|
|
6
10
|
|
|
7
11
|
import yaml
|
|
8
12
|
from rich.console import Console
|
|
9
13
|
from rich.logging import RichHandler
|
|
14
|
+
from rich.style import Style
|
|
15
|
+
from rich.theme import Theme
|
|
16
|
+
|
|
17
|
+
__all__ = ['CONFIG', 'change_logging_level']
|
|
10
18
|
|
|
11
19
|
logger = logging.getLogger('flixopt')
|
|
12
20
|
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
# SINGLE SOURCE OF TRUTH - immutable to prevent accidental modification
|
|
23
|
+
_DEFAULTS = MappingProxyType(
|
|
24
|
+
{
|
|
25
|
+
'config_name': 'flixopt',
|
|
26
|
+
'logging': MappingProxyType(
|
|
27
|
+
{
|
|
28
|
+
'level': 'INFO',
|
|
29
|
+
'file': None,
|
|
30
|
+
'rich': False,
|
|
31
|
+
'console': False,
|
|
32
|
+
'max_file_size': 10_485_760, # 10MB
|
|
33
|
+
'backup_count': 5,
|
|
34
|
+
'date_format': '%Y-%m-%d %H:%M:%S',
|
|
35
|
+
'format': '%(message)s',
|
|
36
|
+
'console_width': 120,
|
|
37
|
+
'show_path': False,
|
|
38
|
+
'show_logger_name': False,
|
|
39
|
+
'colors': MappingProxyType(
|
|
40
|
+
{
|
|
41
|
+
'DEBUG': '\033[90m', # Bright Black/Gray
|
|
42
|
+
'INFO': '\033[0m', # Default/White
|
|
43
|
+
'WARNING': '\033[33m', # Yellow
|
|
44
|
+
'ERROR': '\033[31m', # Red
|
|
45
|
+
'CRITICAL': '\033[1m\033[31m', # Bold Red
|
|
46
|
+
}
|
|
47
|
+
),
|
|
48
|
+
}
|
|
49
|
+
),
|
|
50
|
+
'modeling': MappingProxyType(
|
|
51
|
+
{
|
|
52
|
+
'big': 10_000_000,
|
|
53
|
+
'epsilon': 1e-5,
|
|
54
|
+
'big_binary_bound': 100_000,
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
}
|
|
58
|
+
)
|
|
31
59
|
|
|
32
60
|
|
|
33
|
-
|
|
34
|
-
"""
|
|
35
|
-
|
|
61
|
+
class CONFIG:
|
|
62
|
+
"""Configuration for flixopt library.
|
|
63
|
+
|
|
64
|
+
Always call ``CONFIG.apply()`` after changes.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
Logging: Logging configuration.
|
|
68
|
+
Modeling: Optimization modeling parameters.
|
|
69
|
+
config_name: Configuration name.
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
```python
|
|
73
|
+
CONFIG.Logging.console = True
|
|
74
|
+
CONFIG.Logging.level = 'DEBUG'
|
|
75
|
+
CONFIG.apply()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Load from YAML file:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
logging:
|
|
82
|
+
level: DEBUG
|
|
83
|
+
console: true
|
|
84
|
+
file: app.log
|
|
85
|
+
```
|
|
36
86
|
"""
|
|
37
|
-
if not is_dataclass(cls):
|
|
38
|
-
raise TypeError(f'{cls} must be a dataclass')
|
|
39
|
-
|
|
40
|
-
# Build kwargs for the dataclass constructor
|
|
41
|
-
kwargs = {}
|
|
42
|
-
for field in fields(cls):
|
|
43
|
-
field_name = field.name
|
|
44
|
-
field_type = field.type
|
|
45
|
-
field_value = data.get(field_name)
|
|
46
|
-
|
|
47
|
-
# If the field type is a dataclass and the value is a dict, recursively initialize
|
|
48
|
-
if is_dataclass(field_type) and isinstance(field_value, dict):
|
|
49
|
-
kwargs[field_name] = dataclass_from_dict_with_validation(field_type, field_value)
|
|
50
|
-
else:
|
|
51
|
-
kwargs[field_name] = field_value # Pass as-is if no special handling is needed
|
|
52
|
-
|
|
53
|
-
return cls(**kwargs)
|
|
54
87
|
|
|
88
|
+
class Logging:
|
|
89
|
+
"""Logging configuration.
|
|
90
|
+
|
|
91
|
+
Silent by default. Enable via ``console=True`` or ``file='path'``.
|
|
92
|
+
|
|
93
|
+
Attributes:
|
|
94
|
+
level: Logging level.
|
|
95
|
+
file: Log file path for file logging.
|
|
96
|
+
console: Enable console output.
|
|
97
|
+
rich: Use Rich library for enhanced output.
|
|
98
|
+
max_file_size: Max file size before rotation.
|
|
99
|
+
backup_count: Number of backup files to keep.
|
|
100
|
+
date_format: Date/time format string.
|
|
101
|
+
format: Log message format string.
|
|
102
|
+
console_width: Console width for Rich handler.
|
|
103
|
+
show_path: Show file paths in messages.
|
|
104
|
+
show_logger_name: Show logger name in messages.
|
|
105
|
+
Colors: ANSI color codes for log levels.
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
```python
|
|
109
|
+
# File logging with rotation
|
|
110
|
+
CONFIG.Logging.file = 'app.log'
|
|
111
|
+
CONFIG.Logging.max_file_size = 5_242_880 # 5MB
|
|
112
|
+
CONFIG.apply()
|
|
113
|
+
|
|
114
|
+
# Rich handler with stdout
|
|
115
|
+
CONFIG.Logging.console = True # or 'stdout'
|
|
116
|
+
CONFIG.Logging.rich = True
|
|
117
|
+
CONFIG.apply()
|
|
118
|
+
|
|
119
|
+
# Console output to stderr
|
|
120
|
+
CONFIG.Logging.console = 'stderr'
|
|
121
|
+
CONFIG.apply()
|
|
122
|
+
```
|
|
123
|
+
"""
|
|
55
124
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
125
|
+
level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = _DEFAULTS['logging']['level']
|
|
126
|
+
file: str | None = _DEFAULTS['logging']['file']
|
|
127
|
+
rich: bool = _DEFAULTS['logging']['rich']
|
|
128
|
+
console: bool | Literal['stdout', 'stderr'] = _DEFAULTS['logging']['console']
|
|
129
|
+
max_file_size: int = _DEFAULTS['logging']['max_file_size']
|
|
130
|
+
backup_count: int = _DEFAULTS['logging']['backup_count']
|
|
131
|
+
date_format: str = _DEFAULTS['logging']['date_format']
|
|
132
|
+
format: str = _DEFAULTS['logging']['format']
|
|
133
|
+
console_width: int = _DEFAULTS['logging']['console_width']
|
|
134
|
+
show_path: bool = _DEFAULTS['logging']['show_path']
|
|
135
|
+
show_logger_name: bool = _DEFAULTS['logging']['show_logger_name']
|
|
136
|
+
|
|
137
|
+
class Colors:
|
|
138
|
+
"""ANSI color codes for log levels.
|
|
139
|
+
|
|
140
|
+
Attributes:
|
|
141
|
+
DEBUG: ANSI color for DEBUG level.
|
|
142
|
+
INFO: ANSI color for INFO level.
|
|
143
|
+
WARNING: ANSI color for WARNING level.
|
|
144
|
+
ERROR: ANSI color for ERROR level.
|
|
145
|
+
CRITICAL: ANSI color for CRITICAL level.
|
|
146
|
+
|
|
147
|
+
Examples:
|
|
148
|
+
```python
|
|
149
|
+
CONFIG.Logging.Colors.INFO = '\\033[32m' # Green
|
|
150
|
+
CONFIG.Logging.Colors.ERROR = '\\033[1m\\033[31m' # Bold red
|
|
151
|
+
CONFIG.apply()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Common ANSI codes:
|
|
155
|
+
- '\\033[30m' - Black
|
|
156
|
+
- '\\033[31m' - Red
|
|
157
|
+
- '\\033[32m' - Green
|
|
158
|
+
- '\\033[33m' - Yellow
|
|
159
|
+
- '\\033[34m' - Blue
|
|
160
|
+
- '\\033[35m' - Magenta
|
|
161
|
+
- '\\033[36m' - Cyan
|
|
162
|
+
- '\\033[37m' - White
|
|
163
|
+
- '\\033[90m' - Bright Black/Gray
|
|
164
|
+
- '\\033[0m' - Reset to default
|
|
165
|
+
- '\\033[1m\\033[3Xm' - Bold (replace X with color code 0-7)
|
|
166
|
+
- '\\033[2m\\033[3Xm' - Dim (replace X with color code 0-7)
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
DEBUG: str = _DEFAULTS['logging']['colors']['DEBUG']
|
|
170
|
+
INFO: str = _DEFAULTS['logging']['colors']['INFO']
|
|
171
|
+
WARNING: str = _DEFAULTS['logging']['colors']['WARNING']
|
|
172
|
+
ERROR: str = _DEFAULTS['logging']['colors']['ERROR']
|
|
173
|
+
CRITICAL: str = _DEFAULTS['logging']['colors']['CRITICAL']
|
|
174
|
+
|
|
175
|
+
class Modeling:
|
|
176
|
+
"""Optimization modeling parameters.
|
|
177
|
+
|
|
178
|
+
Attributes:
|
|
179
|
+
big: Large number for big-M constraints.
|
|
180
|
+
epsilon: Tolerance for numerical comparisons.
|
|
181
|
+
big_binary_bound: Upper bound for binary constraints.
|
|
182
|
+
"""
|
|
63
183
|
|
|
184
|
+
big: int = _DEFAULTS['modeling']['big']
|
|
185
|
+
epsilon: float = _DEFAULTS['modeling']['epsilon']
|
|
186
|
+
big_binary_bound: int = _DEFAULTS['modeling']['big_binary_bound']
|
|
64
187
|
|
|
65
|
-
|
|
66
|
-
class LoggingConfig(ValidatedConfig):
|
|
67
|
-
level: Annotated[
|
|
68
|
-
Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
|
69
|
-
lambda level: level in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
|
70
|
-
]
|
|
71
|
-
file: Annotated[str, lambda file: isinstance(file, str)]
|
|
72
|
-
rich: Annotated[bool, lambda rich: isinstance(rich, bool)]
|
|
188
|
+
config_name: str = _DEFAULTS['config_name']
|
|
73
189
|
|
|
190
|
+
@classmethod
|
|
191
|
+
def reset(cls):
|
|
192
|
+
"""Reset all configuration values to defaults."""
|
|
193
|
+
for key, value in _DEFAULTS['logging'].items():
|
|
194
|
+
if key == 'colors':
|
|
195
|
+
# Reset nested Colors class
|
|
196
|
+
for color_key, color_value in value.items():
|
|
197
|
+
setattr(cls.Logging.Colors, color_key, color_value)
|
|
198
|
+
else:
|
|
199
|
+
setattr(cls.Logging, key, value)
|
|
200
|
+
|
|
201
|
+
for key, value in _DEFAULTS['modeling'].items():
|
|
202
|
+
setattr(cls.Modeling, key, value)
|
|
203
|
+
|
|
204
|
+
cls.config_name = _DEFAULTS['config_name']
|
|
205
|
+
cls.apply()
|
|
74
206
|
|
|
75
|
-
@
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
207
|
+
@classmethod
|
|
208
|
+
def apply(cls):
|
|
209
|
+
"""Apply current configuration to logging system."""
|
|
210
|
+
# Convert Colors class attributes to dict
|
|
211
|
+
colors_dict = {
|
|
212
|
+
'DEBUG': cls.Logging.Colors.DEBUG,
|
|
213
|
+
'INFO': cls.Logging.Colors.INFO,
|
|
214
|
+
'WARNING': cls.Logging.Colors.WARNING,
|
|
215
|
+
'ERROR': cls.Logging.Colors.ERROR,
|
|
216
|
+
'CRITICAL': cls.Logging.Colors.CRITICAL,
|
|
217
|
+
}
|
|
218
|
+
valid_levels = list(colors_dict)
|
|
219
|
+
if cls.Logging.level.upper() not in valid_levels:
|
|
220
|
+
raise ValueError(f"Invalid log level '{cls.Logging.level}'. Must be one of: {', '.join(valid_levels)}")
|
|
221
|
+
|
|
222
|
+
if cls.Logging.max_file_size <= 0:
|
|
223
|
+
raise ValueError('max_file_size must be positive')
|
|
224
|
+
|
|
225
|
+
if cls.Logging.backup_count < 0:
|
|
226
|
+
raise ValueError('backup_count must be non-negative')
|
|
227
|
+
|
|
228
|
+
if cls.Logging.console not in (False, True, 'stdout', 'stderr'):
|
|
229
|
+
raise ValueError(f"console must be False, True, 'stdout', or 'stderr', got {cls.Logging.console}")
|
|
230
|
+
|
|
231
|
+
_setup_logging(
|
|
232
|
+
default_level=cls.Logging.level,
|
|
233
|
+
log_file=cls.Logging.file,
|
|
234
|
+
use_rich_handler=cls.Logging.rich,
|
|
235
|
+
console=cls.Logging.console,
|
|
236
|
+
max_file_size=cls.Logging.max_file_size,
|
|
237
|
+
backup_count=cls.Logging.backup_count,
|
|
238
|
+
date_format=cls.Logging.date_format,
|
|
239
|
+
format=cls.Logging.format,
|
|
240
|
+
console_width=cls.Logging.console_width,
|
|
241
|
+
show_path=cls.Logging.show_path,
|
|
242
|
+
show_logger_name=cls.Logging.show_logger_name,
|
|
243
|
+
colors=colors_dict,
|
|
244
|
+
)
|
|
80
245
|
|
|
246
|
+
@classmethod
|
|
247
|
+
def load_from_file(cls, config_file: str | Path):
|
|
248
|
+
"""Load configuration from YAML file and apply it.
|
|
81
249
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
config_name: Annotated[str, lambda x: isinstance(x, str)]
|
|
85
|
-
logging: LoggingConfig
|
|
86
|
-
modeling: ModelingConfig
|
|
250
|
+
Args:
|
|
251
|
+
config_file: Path to the YAML configuration file.
|
|
87
252
|
|
|
253
|
+
Raises:
|
|
254
|
+
FileNotFoundError: If the config file does not exist.
|
|
255
|
+
"""
|
|
256
|
+
config_path = Path(config_file)
|
|
257
|
+
if not config_path.exists():
|
|
258
|
+
raise FileNotFoundError(f'Config file not found: {config_file}')
|
|
88
259
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"""
|
|
260
|
+
with config_path.open() as file:
|
|
261
|
+
config_dict = yaml.safe_load(file) or {}
|
|
262
|
+
cls._apply_config_dict(config_dict)
|
|
93
263
|
|
|
94
|
-
|
|
95
|
-
modeling: ModelingConfig = None
|
|
96
|
-
logging: LoggingConfig = None
|
|
264
|
+
cls.apply()
|
|
97
265
|
|
|
98
266
|
@classmethod
|
|
99
|
-
def
|
|
100
|
-
"""
|
|
101
|
-
|
|
267
|
+
def _apply_config_dict(cls, config_dict: dict):
|
|
268
|
+
"""Apply configuration dictionary to class attributes.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
config_dict: Dictionary containing configuration values.
|
|
102
272
|
"""
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
273
|
+
for key, value in config_dict.items():
|
|
274
|
+
if key == 'logging' and isinstance(value, dict):
|
|
275
|
+
for nested_key, nested_value in value.items():
|
|
276
|
+
if nested_key == 'colors' and isinstance(nested_value, dict):
|
|
277
|
+
# Handle nested colors under logging
|
|
278
|
+
for color_key, color_value in nested_value.items():
|
|
279
|
+
setattr(cls.Logging.Colors, color_key, color_value)
|
|
280
|
+
else:
|
|
281
|
+
setattr(cls.Logging, nested_key, nested_value)
|
|
282
|
+
elif key == 'modeling' and isinstance(value, dict):
|
|
283
|
+
for nested_key, nested_value in value.items():
|
|
284
|
+
setattr(cls.Modeling, nested_key, nested_value)
|
|
285
|
+
elif hasattr(cls, key):
|
|
286
|
+
setattr(cls, key, value)
|
|
114
287
|
|
|
115
|
-
|
|
116
|
-
|
|
288
|
+
@classmethod
|
|
289
|
+
def to_dict(cls) -> dict:
|
|
290
|
+
"""Convert the configuration class into a dictionary for JSON serialization.
|
|
117
291
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
292
|
+
Returns:
|
|
293
|
+
Dictionary representation of the current configuration.
|
|
294
|
+
"""
|
|
295
|
+
return {
|
|
296
|
+
'config_name': cls.config_name,
|
|
297
|
+
'logging': {
|
|
298
|
+
'level': cls.Logging.level,
|
|
299
|
+
'file': cls.Logging.file,
|
|
300
|
+
'rich': cls.Logging.rich,
|
|
301
|
+
'console': cls.Logging.console,
|
|
302
|
+
'max_file_size': cls.Logging.max_file_size,
|
|
303
|
+
'backup_count': cls.Logging.backup_count,
|
|
304
|
+
'date_format': cls.Logging.date_format,
|
|
305
|
+
'format': cls.Logging.format,
|
|
306
|
+
'console_width': cls.Logging.console_width,
|
|
307
|
+
'show_path': cls.Logging.show_path,
|
|
308
|
+
'show_logger_name': cls.Logging.show_logger_name,
|
|
309
|
+
'colors': {
|
|
310
|
+
'DEBUG': cls.Logging.Colors.DEBUG,
|
|
311
|
+
'INFO': cls.Logging.Colors.INFO,
|
|
312
|
+
'WARNING': cls.Logging.Colors.WARNING,
|
|
313
|
+
'ERROR': cls.Logging.Colors.ERROR,
|
|
314
|
+
'CRITICAL': cls.Logging.Colors.CRITICAL,
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
'modeling': {
|
|
318
|
+
'big': cls.Modeling.big,
|
|
319
|
+
'epsilon': cls.Modeling.epsilon,
|
|
320
|
+
'big_binary_bound': cls.Modeling.big_binary_bound,
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class MultilineFormatter(logging.Formatter):
|
|
326
|
+
"""Formatter that handles multi-line messages with consistent prefixes.
|
|
122
327
|
|
|
123
|
-
|
|
328
|
+
Args:
|
|
329
|
+
fmt: Log message format string.
|
|
330
|
+
datefmt: Date/time format string.
|
|
331
|
+
show_logger_name: Show logger name in log messages.
|
|
332
|
+
"""
|
|
124
333
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
Convert the configuration class into a dictionary for JSON serialization.
|
|
129
|
-
Handles dataclasses and simple types like str, int, etc.
|
|
130
|
-
"""
|
|
131
|
-
config_dict = {}
|
|
132
|
-
for attribute, value in cls.__dict__.items():
|
|
133
|
-
# Only consider attributes (not methods, etc.)
|
|
134
|
-
if (
|
|
135
|
-
not attribute.startswith('_')
|
|
136
|
-
and not isinstance(value, (types.FunctionType, types.MethodType))
|
|
137
|
-
and not isinstance(value, classmethod)
|
|
138
|
-
):
|
|
139
|
-
if is_dataclass(value):
|
|
140
|
-
config_dict[attribute] = value.__dict__
|
|
141
|
-
else: # Assuming only basic types here!
|
|
142
|
-
config_dict[attribute] = value
|
|
143
|
-
|
|
144
|
-
return config_dict
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
class MultilineFormater(logging.Formatter):
|
|
148
|
-
def format(self, record):
|
|
149
|
-
message_lines = record.getMessage().split('\n')
|
|
334
|
+
def __init__(self, fmt: str = '%(message)s', datefmt: str | None = None, show_logger_name: bool = False):
|
|
335
|
+
super().__init__(fmt=fmt, datefmt=datefmt)
|
|
336
|
+
self.show_logger_name = show_logger_name
|
|
150
337
|
|
|
151
|
-
|
|
338
|
+
def format(self, record) -> str:
|
|
339
|
+
record.message = record.getMessage()
|
|
340
|
+
message_lines = self._style.format(record).split('\n')
|
|
152
341
|
timestamp = self.formatTime(record, self.datefmt)
|
|
153
|
-
log_level = record.levelname.ljust(8)
|
|
154
|
-
log_prefix = f'{timestamp} | {log_level} |'
|
|
342
|
+
log_level = record.levelname.ljust(8)
|
|
155
343
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
344
|
+
if self.show_logger_name:
|
|
345
|
+
# Truncate long logger names for readability
|
|
346
|
+
logger_name = record.name if len(record.name) <= 20 else f'...{record.name[-17:]}'
|
|
347
|
+
log_prefix = f'{timestamp} | {log_level} | {logger_name.ljust(20)} |'
|
|
160
348
|
else:
|
|
161
|
-
|
|
349
|
+
log_prefix = f'{timestamp} | {log_level} |'
|
|
350
|
+
|
|
351
|
+
indent = ' ' * (len(log_prefix) + 1) # +1 for the space after prefix
|
|
352
|
+
|
|
353
|
+
lines = [f'{log_prefix} {message_lines[0]}']
|
|
354
|
+
if len(message_lines) > 1:
|
|
355
|
+
lines.extend([f'{indent}{line}' for line in message_lines[1:]])
|
|
162
356
|
|
|
163
357
|
return '\n'.join(lines)
|
|
164
358
|
|
|
165
359
|
|
|
166
|
-
class
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
360
|
+
class ColoredMultilineFormatter(MultilineFormatter):
|
|
361
|
+
"""Formatter that adds ANSI colors to multi-line log messages.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
fmt: Log message format string.
|
|
365
|
+
datefmt: Date/time format string.
|
|
366
|
+
colors: Dictionary of ANSI color codes for each log level.
|
|
367
|
+
show_logger_name: Show logger name in log messages.
|
|
368
|
+
"""
|
|
369
|
+
|
|
175
370
|
RESET = '\033[0m'
|
|
176
371
|
|
|
372
|
+
def __init__(
|
|
373
|
+
self,
|
|
374
|
+
fmt: str | None = None,
|
|
375
|
+
datefmt: str | None = None,
|
|
376
|
+
colors: dict[str, str] | None = None,
|
|
377
|
+
show_logger_name: bool = False,
|
|
378
|
+
):
|
|
379
|
+
super().__init__(fmt=fmt, datefmt=datefmt, show_logger_name=show_logger_name)
|
|
380
|
+
self.COLORS = (
|
|
381
|
+
colors
|
|
382
|
+
if colors is not None
|
|
383
|
+
else {
|
|
384
|
+
'DEBUG': '\033[90m',
|
|
385
|
+
'INFO': '\033[0m',
|
|
386
|
+
'WARNING': '\033[33m',
|
|
387
|
+
'ERROR': '\033[31m',
|
|
388
|
+
'CRITICAL': '\033[1m\033[31m',
|
|
389
|
+
}
|
|
390
|
+
)
|
|
391
|
+
|
|
177
392
|
def format(self, record):
|
|
178
393
|
lines = super().format(record).splitlines()
|
|
179
394
|
log_color = self.COLORS.get(record.levelname, self.RESET)
|
|
395
|
+
formatted_lines = [f'{log_color}{line}{self.RESET}' for line in lines]
|
|
396
|
+
return '\n'.join(formatted_lines)
|
|
180
397
|
|
|
181
|
-
# Create a formatted message for each line separately
|
|
182
|
-
formatted_lines = []
|
|
183
|
-
for line in lines:
|
|
184
|
-
formatted_lines.append(f'{log_color}{line}{self.RESET}')
|
|
185
398
|
|
|
186
|
-
|
|
399
|
+
def _create_console_handler(
|
|
400
|
+
use_rich: bool = False,
|
|
401
|
+
stream: Literal['stdout', 'stderr'] = 'stdout',
|
|
402
|
+
console_width: int = 120,
|
|
403
|
+
show_path: bool = False,
|
|
404
|
+
show_logger_name: bool = False,
|
|
405
|
+
date_format: str = '%Y-%m-%d %H:%M:%S',
|
|
406
|
+
format: str = '%(message)s',
|
|
407
|
+
colors: dict[str, str] | None = None,
|
|
408
|
+
) -> logging.Handler:
|
|
409
|
+
"""Create a console logging handler.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
use_rich: If True, use RichHandler with color support.
|
|
413
|
+
stream: Output stream
|
|
414
|
+
console_width: Width of the console for Rich handler.
|
|
415
|
+
show_path: Show file paths in log messages (Rich only).
|
|
416
|
+
show_logger_name: Show logger name in log messages.
|
|
417
|
+
date_format: Date/time format string.
|
|
418
|
+
format: Log message format string.
|
|
419
|
+
colors: Dictionary of ANSI color codes for each log level.
|
|
187
420
|
|
|
421
|
+
Returns:
|
|
422
|
+
Configured logging handler (RichHandler or StreamHandler).
|
|
423
|
+
"""
|
|
424
|
+
# Determine the stream object
|
|
425
|
+
stream_obj = sys.stdout if stream == 'stdout' else sys.stderr
|
|
426
|
+
|
|
427
|
+
if use_rich:
|
|
428
|
+
# Convert ANSI codes to Rich theme
|
|
429
|
+
if colors:
|
|
430
|
+
theme_dict = {}
|
|
431
|
+
for level, ansi_code in colors.items():
|
|
432
|
+
# Rich can parse ANSI codes directly!
|
|
433
|
+
try:
|
|
434
|
+
style = Style.from_ansi(ansi_code)
|
|
435
|
+
theme_dict[f'logging.level.{level.lower()}'] = style
|
|
436
|
+
except Exception:
|
|
437
|
+
# Fallback to default if parsing fails
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
theme = Theme(theme_dict) if theme_dict else None
|
|
441
|
+
else:
|
|
442
|
+
theme = None
|
|
188
443
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if use_rich_handler and log_file is None:
|
|
192
|
-
# RichHandler for console output
|
|
193
|
-
console = Console(width=120)
|
|
194
|
-
rich_handler = RichHandler(
|
|
444
|
+
console = Console(width=console_width, theme=theme, file=stream_obj)
|
|
445
|
+
handler = RichHandler(
|
|
195
446
|
console=console,
|
|
196
447
|
rich_tracebacks=True,
|
|
197
448
|
omit_repeated_times=True,
|
|
198
|
-
show_path=
|
|
199
|
-
log_time_format=
|
|
200
|
-
)
|
|
201
|
-
rich_handler.setFormatter(logging.Formatter('%(message)s')) # Simplified formatting
|
|
202
|
-
|
|
203
|
-
return rich_handler
|
|
204
|
-
elif log_file is None:
|
|
205
|
-
# Regular Logger with custom formating enabled
|
|
206
|
-
file_handler = logging.StreamHandler()
|
|
207
|
-
file_handler.setFormatter(
|
|
208
|
-
ColoredMultilineFormater(
|
|
209
|
-
fmt='%(message)s',
|
|
210
|
-
datefmt='%Y-%m-%d %H:%M:%S',
|
|
211
|
-
)
|
|
449
|
+
show_path=show_path,
|
|
450
|
+
log_time_format=date_format,
|
|
212
451
|
)
|
|
213
|
-
|
|
452
|
+
handler.setFormatter(logging.Formatter(format))
|
|
214
453
|
else:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
454
|
+
handler = logging.StreamHandler(stream=stream_obj)
|
|
455
|
+
handler.setFormatter(
|
|
456
|
+
ColoredMultilineFormatter(
|
|
457
|
+
fmt=format,
|
|
458
|
+
datefmt=date_format,
|
|
459
|
+
colors=colors,
|
|
460
|
+
show_logger_name=show_logger_name,
|
|
221
461
|
)
|
|
222
462
|
)
|
|
223
|
-
|
|
463
|
+
|
|
464
|
+
return handler
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _create_file_handler(
|
|
468
|
+
log_file: str,
|
|
469
|
+
max_file_size: int = 10_485_760,
|
|
470
|
+
backup_count: int = 5,
|
|
471
|
+
show_logger_name: bool = False,
|
|
472
|
+
date_format: str = '%Y-%m-%d %H:%M:%S',
|
|
473
|
+
format: str = '%(message)s',
|
|
474
|
+
) -> RotatingFileHandler:
|
|
475
|
+
"""Create a rotating file handler to prevent huge log files.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
log_file: Path to the log file.
|
|
479
|
+
max_file_size: Maximum size in bytes before rotation.
|
|
480
|
+
backup_count: Number of backup files to keep.
|
|
481
|
+
show_logger_name: Show logger name in log messages.
|
|
482
|
+
date_format: Date/time format string.
|
|
483
|
+
format: Log message format string.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Configured RotatingFileHandler (without colors).
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
# Ensure parent directory exists
|
|
490
|
+
log_path = Path(log_file)
|
|
491
|
+
try:
|
|
492
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
493
|
+
except PermissionError as e:
|
|
494
|
+
raise PermissionError(f"Cannot create log directory '{log_path.parent}': Permission denied") from e
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
handler = RotatingFileHandler(
|
|
498
|
+
log_file,
|
|
499
|
+
maxBytes=max_file_size,
|
|
500
|
+
backupCount=backup_count,
|
|
501
|
+
encoding='utf-8',
|
|
502
|
+
)
|
|
503
|
+
except PermissionError as e:
|
|
504
|
+
raise PermissionError(
|
|
505
|
+
f"Cannot write to log file '{log_file}': Permission denied. "
|
|
506
|
+
f'Choose a different location or check file permissions.'
|
|
507
|
+
) from e
|
|
508
|
+
|
|
509
|
+
handler.setFormatter(
|
|
510
|
+
MultilineFormatter(
|
|
511
|
+
fmt=format,
|
|
512
|
+
datefmt=date_format,
|
|
513
|
+
show_logger_name=show_logger_name,
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
return handler
|
|
224
517
|
|
|
225
518
|
|
|
226
|
-
def
|
|
519
|
+
def _setup_logging(
|
|
227
520
|
default_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO',
|
|
228
|
-
log_file:
|
|
521
|
+
log_file: str | None = None,
|
|
229
522
|
use_rich_handler: bool = False,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
523
|
+
console: bool | Literal['stdout', 'stderr'] = False,
|
|
524
|
+
max_file_size: int = 10_485_760,
|
|
525
|
+
backup_count: int = 5,
|
|
526
|
+
date_format: str = '%Y-%m-%d %H:%M:%S',
|
|
527
|
+
format: str = '%(message)s',
|
|
528
|
+
console_width: int = 120,
|
|
529
|
+
show_path: bool = False,
|
|
530
|
+
show_logger_name: bool = False,
|
|
531
|
+
colors: dict[str, str] | None = None,
|
|
532
|
+
) -> None:
|
|
533
|
+
"""Internal function to setup logging - use CONFIG.apply() instead.
|
|
534
|
+
|
|
535
|
+
Configures the flixopt logger with console and/or file handlers.
|
|
536
|
+
If no handlers are configured, adds NullHandler (library best practice).
|
|
241
537
|
|
|
242
|
-
|
|
538
|
+
Args:
|
|
539
|
+
default_level: Logging level for the logger.
|
|
540
|
+
log_file: Path to log file (None to disable file logging).
|
|
541
|
+
use_rich_handler: Use Rich for enhanced console output.
|
|
542
|
+
console: Enable console logging.
|
|
543
|
+
max_file_size: Maximum log file size before rotation.
|
|
544
|
+
backup_count: Number of backup log files to keep.
|
|
545
|
+
date_format: Date/time format for log messages.
|
|
546
|
+
format: Log message format string.
|
|
547
|
+
console_width: Console width for Rich handler.
|
|
548
|
+
show_path: Show file paths in log messages (Rich only).
|
|
549
|
+
show_logger_name: Show logger name in log messages.
|
|
550
|
+
colors: ANSI color codes for each log level.
|
|
551
|
+
"""
|
|
552
|
+
logger = logging.getLogger('flixopt')
|
|
553
|
+
logger.setLevel(getattr(logging, default_level.upper()))
|
|
554
|
+
logger.propagate = False # Prevent duplicate logs
|
|
555
|
+
logger.handlers.clear()
|
|
556
|
+
|
|
557
|
+
# Handle console parameter: False = disabled, True = stdout, 'stdout' = stdout, 'stderr' = stderr
|
|
558
|
+
if console:
|
|
559
|
+
# Convert True to 'stdout', keep 'stdout'/'stderr' as-is
|
|
560
|
+
stream = 'stdout' if console is True else console
|
|
561
|
+
logger.addHandler(
|
|
562
|
+
_create_console_handler(
|
|
563
|
+
use_rich=use_rich_handler,
|
|
564
|
+
stream=stream,
|
|
565
|
+
console_width=console_width,
|
|
566
|
+
show_path=show_path,
|
|
567
|
+
show_logger_name=show_logger_name,
|
|
568
|
+
date_format=date_format,
|
|
569
|
+
format=format,
|
|
570
|
+
colors=colors,
|
|
571
|
+
)
|
|
572
|
+
)
|
|
243
573
|
|
|
574
|
+
if log_file:
|
|
575
|
+
logger.addHandler(
|
|
576
|
+
_create_file_handler(
|
|
577
|
+
log_file=log_file,
|
|
578
|
+
max_file_size=max_file_size,
|
|
579
|
+
backup_count=backup_count,
|
|
580
|
+
show_logger_name=show_logger_name,
|
|
581
|
+
date_format=date_format,
|
|
582
|
+
format=format,
|
|
583
|
+
)
|
|
584
|
+
)
|
|
244
585
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
raise ValueError(f'Invalid logging level {level_name}')
|
|
249
|
-
else:
|
|
250
|
-
logging_level = getattr(logging, level_name.upper(), logging.WARNING)
|
|
251
|
-
return logging_level
|
|
586
|
+
# Library best practice: NullHandler if no handlers configured
|
|
587
|
+
if not logger.handlers:
|
|
588
|
+
logger.addHandler(logging.NullHandler())
|
|
252
589
|
|
|
253
590
|
|
|
254
591
|
def change_logging_level(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']):
|
|
592
|
+
"""Change the logging level for the flixopt logger and all its handlers.
|
|
593
|
+
|
|
594
|
+
.. deprecated:: 2.1.11
|
|
595
|
+
Use ``CONFIG.Logging.level = level_name`` and ``CONFIG.apply()`` instead.
|
|
596
|
+
This function will be removed in version 3.0.0.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
level_name: The logging level to set.
|
|
600
|
+
|
|
601
|
+
Examples:
|
|
602
|
+
>>> change_logging_level('DEBUG') # deprecated
|
|
603
|
+
>>> # Use this instead:
|
|
604
|
+
>>> CONFIG.Logging.level = 'DEBUG'
|
|
605
|
+
>>> CONFIG.apply()
|
|
606
|
+
"""
|
|
607
|
+
warnings.warn(
|
|
608
|
+
'change_logging_level is deprecated and will be removed in version 3.0.0. '
|
|
609
|
+
'Use CONFIG.Logging.level = level_name and CONFIG.apply() instead.',
|
|
610
|
+
DeprecationWarning,
|
|
611
|
+
stacklevel=2,
|
|
612
|
+
)
|
|
255
613
|
logger = logging.getLogger('flixopt')
|
|
256
|
-
logging_level =
|
|
614
|
+
logging_level = getattr(logging, level_name.upper())
|
|
257
615
|
logger.setLevel(logging_level)
|
|
258
616
|
for handler in logger.handlers:
|
|
259
617
|
handler.setLevel(logging_level)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
# Initialize default config
|
|
621
|
+
CONFIG.apply()
|