flixopt 2.2.0b0__py3-none-any.whl → 3.0.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 +35 -1
- flixopt/aggregation.py +60 -81
- flixopt/calculation.py +381 -196
- flixopt/components.py +1022 -359
- flixopt/config.py +553 -191
- flixopt/core.py +475 -1315
- flixopt/effects.py +477 -214
- flixopt/elements.py +591 -344
- flixopt/features.py +403 -957
- flixopt/flow_system.py +781 -293
- flixopt/interface.py +1159 -189
- flixopt/io.py +50 -55
- flixopt/linear_converters.py +384 -92
- flixopt/modeling.py +759 -0
- flixopt/network_app.py +789 -0
- flixopt/plotting.py +273 -135
- flixopt/results.py +639 -383
- flixopt/solvers.py +25 -21
- flixopt/structure.py +928 -442
- flixopt/utils.py +34 -5
- flixopt-3.0.0.dist-info/METADATA +209 -0
- flixopt-3.0.0.dist-info/RECORD +26 -0
- {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/WHEEL +1 -1
- flixopt-3.0.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 -49
- 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/release-notes/_template.txt +0 -32
- docs/release-notes/index.md +0 -7
- docs/release-notes/v2.0.0.md +0 -93
- docs/release-notes/v2.0.1.md +0 -12
- docs/release-notes/v2.1.0.md +0 -31
- docs/release-notes/v2.2.0.md +0 -55
- 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/Investment.md +0 -115
- 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.0b0.dist-info/METADATA +0 -146
- flixopt-2.2.0b0.dist-info/RECORD +0 -59
- flixopt-2.2.0b0.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/gen_ref_pages.py +0 -54
- tests/ressources/Zeitreihen2020.csv +0 -35137
- {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.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
87
|
|
|
53
|
-
|
|
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
|
+
"""
|
|
54
124
|
|
|
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
|
+
"""
|
|
55
183
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if field := self.__dataclass_fields__.get(name):
|
|
60
|
-
if metadata := getattr(field.type, '__metadata__', None):
|
|
61
|
-
assert metadata[0](value), f'Invalid value passed to {name!r}: {value=}'
|
|
62
|
-
super().__setattr__(name, value)
|
|
184
|
+
big: int = _DEFAULTS['modeling']['big']
|
|
185
|
+
epsilon: float = _DEFAULTS['modeling']['epsilon']
|
|
186
|
+
big_binary_bound: int = _DEFAULTS['modeling']['big_binary_bound']
|
|
63
187
|
|
|
188
|
+
config_name: str = _DEFAULTS['config_name']
|
|
64
189
|
|
|
65
|
-
@
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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()
|
|
73
206
|
|
|
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
|
+
)
|
|
74
245
|
|
|
75
|
-
@
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
EPSILON: Annotated[float, lambda x: isinstance(x, float)]
|
|
79
|
-
BIG_BINARY_BOUND: Annotated[int, lambda x: isinstance(x, int)]
|
|
246
|
+
@classmethod
|
|
247
|
+
def load_from_file(cls, config_file: str | Path):
|
|
248
|
+
"""Load configuration from YAML file and apply it.
|
|
80
249
|
|
|
250
|
+
Args:
|
|
251
|
+
config_file: Path to the YAML configuration file.
|
|
81
252
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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}')
|
|
87
259
|
|
|
260
|
+
with config_path.open() as file:
|
|
261
|
+
config_dict = yaml.safe_load(file) or {}
|
|
262
|
+
cls._apply_config_dict(config_dict)
|
|
88
263
|
|
|
89
|
-
|
|
90
|
-
"""
|
|
91
|
-
A configuration class that stores global configuration values as class attributes.
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
config_name: str = None
|
|
95
|
-
modeling: ModelingConfig = None
|
|
96
|
-
logging: LoggingConfig = None
|
|
264
|
+
cls.apply()
|
|
97
265
|
|
|
98
266
|
@classmethod
|
|
99
|
-
def
|
|
100
|
-
"""
|
|
101
|
-
Initialize configuration using defaults or user-specified file.
|
|
102
|
-
"""
|
|
103
|
-
# Default config file
|
|
104
|
-
default_config_path = os.path.join(os.path.dirname(__file__), 'config.yaml')
|
|
105
|
-
|
|
106
|
-
if user_config_file is None:
|
|
107
|
-
with open(default_config_path, 'r') as file:
|
|
108
|
-
new_config = yaml.safe_load(file)
|
|
109
|
-
elif not os.path.exists(user_config_file):
|
|
110
|
-
raise FileNotFoundError(f'Config file not found: {user_config_file}')
|
|
111
|
-
else:
|
|
112
|
-
with open(user_config_file, 'r') as user_file:
|
|
113
|
-
new_config = yaml.safe_load(user_file)
|
|
114
|
-
|
|
115
|
-
# Convert the merged config to ConfigSchema
|
|
116
|
-
config_data = dataclass_from_dict_with_validation(ConfigSchema, new_config)
|
|
267
|
+
def _apply_config_dict(cls, config_dict: dict):
|
|
268
|
+
"""Apply configuration dictionary to class attributes.
|
|
117
269
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
270
|
+
Args:
|
|
271
|
+
config_dict: Dictionary containing configuration values.
|
|
272
|
+
"""
|
|
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)
|
|
124
287
|
|
|
125
288
|
@classmethod
|
|
126
289
|
def to_dict(cls):
|
|
290
|
+
"""Convert the configuration class into a dictionary for JSON serialization.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Dictionary representation of the current configuration.
|
|
127
294
|
"""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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.
|
|
150
327
|
|
|
151
|
-
|
|
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
|
+
"""
|
|
333
|
+
|
|
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
|
|
337
|
+
|
|
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.
|
|
187
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.
|
|
188
420
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
443
|
+
|
|
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
|
-
return file_handler
|
|
224
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
|
+
"""
|
|
225
488
|
|
|
226
|
-
|
|
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
|
|
517
|
+
|
|
518
|
+
|
|
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()
|