flixopt 2.1.11__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flixopt might be problematic. Click here for more details.

Files changed (34) hide show
  1. {flixopt-2.1.11 → flixopt-2.2.0}/CHANGELOG.md +27 -1
  2. {flixopt-2.1.11/flixopt.egg-info → flixopt-2.2.0}/PKG-INFO +4 -4
  3. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/__init__.py +0 -2
  4. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/calculation.py +2 -2
  5. flixopt-2.2.0/flixopt/config.py +561 -0
  6. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/elements.py +5 -5
  7. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/features.py +6 -6
  8. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/interface.py +4 -4
  9. {flixopt-2.1.11 → flixopt-2.2.0/flixopt.egg-info}/PKG-INFO +4 -4
  10. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt.egg-info/SOURCES.txt +0 -1
  11. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt.egg-info/requires.txt +3 -3
  12. {flixopt-2.1.11 → flixopt-2.2.0}/pyproject.toml +3 -3
  13. flixopt-2.1.11/flixopt/config.py +0 -268
  14. flixopt-2.1.11/flixopt/config.yaml +0 -10
  15. {flixopt-2.1.11 → flixopt-2.2.0}/LICENSE +0 -0
  16. {flixopt-2.1.11 → flixopt-2.2.0}/MANIFEST.in +0 -0
  17. {flixopt-2.1.11 → flixopt-2.2.0}/README.md +0 -0
  18. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/aggregation.py +0 -0
  19. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/commons.py +0 -0
  20. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/components.py +0 -0
  21. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/core.py +0 -0
  22. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/effects.py +0 -0
  23. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/flow_system.py +0 -0
  24. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/io.py +0 -0
  25. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/linear_converters.py +0 -0
  26. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/network_app.py +0 -0
  27. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/plotting.py +0 -0
  28. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/results.py +0 -0
  29. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/solvers.py +0 -0
  30. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/structure.py +0 -0
  31. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt/utils.py +0 -0
  32. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt.egg-info/dependency_links.txt +0 -0
  33. {flixopt-2.1.11 → flixopt-2.2.0}/flixopt.egg-info/top_level.txt +0 -0
  34. {flixopt-2.1.11 → flixopt-2.2.0}/setup.cfg +0 -0
@@ -4,6 +4,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
4
4
  Formatting is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) & [Gitmoji](https://gitmoji.dev).
5
5
  For more details regarding the individual PRs and contributors, please refer to our [GitHub releases](https://github.com/flixOpt/flixopt/releases).
6
6
 
7
+ ---
8
+
7
9
  <!-- This text won't be rendered
8
10
  Note: The CI will automatically append a "What's Changed" section to the changelog for github releases.
9
11
  This contains all commits, PRs, and contributors.
@@ -46,7 +48,6 @@ Please keep the format of the changelog consistent with the other releases, so t
46
48
  ### 💥 Breaking Changes
47
49
 
48
50
  ### ♻️ Changed
49
- - Using `h5netcdf` instead of `netCDF4` for dataset I/O operations. This follows the update in `xarray==2025.09.01`
50
51
 
51
52
  ### 🗑️ Deprecated
52
53
 
@@ -65,6 +66,30 @@ Please keep the format of the changelog consistent with the other releases, so t
65
66
  ### 🚧 Known Issues
66
67
 
67
68
  Until here -->
69
+
70
+ ## [2.2.0] - 2025-10-11
71
+ **Summary:** This release is a Configuration and Logging management release.
72
+
73
+ ### ✨ Added
74
+ - Added `CONFIG.reset()` method to restore configuration to default values
75
+ - Added configurable log file rotation settings: `CONFIG.Logging.max_file_size` and `CONFIG.Logging.backup_count`
76
+ - Added configurable log format settings: `CONFIG.Logging.date_format` and `CONFIG.Logging.format`
77
+ - Added configurable console settings: `CONFIG.Logging.console_width` and `CONFIG.Logging.show_path`
78
+ - Added `CONFIG.Logging.Colors` nested class for customizable log level colors using ANSI escape codes (works with both standard and Rich handlers)
79
+
80
+ ### ♻️ Changed
81
+ - Logging and Configuration management changed
82
+
83
+ ### 🗑️ Deprecated
84
+ - `change_logging_level()` function is now deprecated in favor of `CONFIG.Logging.level` and `CONFIG.apply()`. Will be removed in version 3.0.0.
85
+
86
+ ### 🔥 Removed
87
+ - Removed unused `config.merge_configs` function from configuration module
88
+
89
+ ### 👷 Development
90
+ - Greatly expanded test coverage for `config.py` module
91
+ - Added `@pytest.mark.xdist_group` to `TestConfigModule` tests to prevent global config interference
92
+
68
93
  ---
69
94
 
70
95
  ## [2.1.11] - 2025-10-05
@@ -78,6 +103,7 @@ Until here -->
78
103
 
79
104
  ### 📦 Dependencies
80
105
  - Updated `renovate.config` to treat CalVer packages (xarray and dask) with more care
106
+ - Updated packaging configuration
81
107
 
82
108
  ---
83
109
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flixopt
3
- Version: 2.1.11
3
+ Version: 2.2.0
4
4
  Summary: Vector based energy and material flow optimization framework in Python.
5
5
  Author-email: "Chair of Building Energy Systems and Heat Supply, TU Dresden" <peter.stange@tu-dresden.de>, Felix Bumann <felixbumann387@gmail.com>, Felix Panitz <baumbude@googlemail.com>, Peter Stange <peter.stange@tu-dresden.de>
6
6
  Maintainer-email: Felix Bumann <felixbumann387@gmail.com>, Peter Stange <peter.stange@tu-dresden.de>
@@ -53,10 +53,10 @@ Provides-Extra: dev
53
53
  Requires-Dist: pytest==8.4.2; extra == "dev"
54
54
  Requires-Dist: pytest-xdist==3.8.0; extra == "dev"
55
55
  Requires-Dist: nbformat==5.10.4; extra == "dev"
56
- Requires-Dist: ruff==0.13.0; extra == "dev"
56
+ Requires-Dist: ruff==0.13.2; extra == "dev"
57
57
  Requires-Dist: pre-commit==4.3.0; extra == "dev"
58
58
  Requires-Dist: pyvis==0.3.2; extra == "dev"
59
- Requires-Dist: tsam==2.3.1; extra == "dev"
59
+ Requires-Dist: tsam==2.3.9; extra == "dev"
60
60
  Requires-Dist: scipy==1.15.1; extra == "dev"
61
61
  Requires-Dist: gurobipy==12.0.3; extra == "dev"
62
62
  Requires-Dist: dash==3.0.0; extra == "dev"
@@ -65,7 +65,7 @@ Requires-Dist: dash-daq==0.6.0; extra == "dev"
65
65
  Requires-Dist: networkx==3.0.0; extra == "dev"
66
66
  Requires-Dist: werkzeug==3.0.0; extra == "dev"
67
67
  Provides-Extra: docs
68
- Requires-Dist: mkdocs-material==9.6.19; extra == "docs"
68
+ Requires-Dist: mkdocs-material==9.6.20; extra == "docs"
69
69
  Requires-Dist: mkdocstrings-python==1.18.2; extra == "docs"
70
70
  Requires-Dist: mkdocs-table-reader-plugin==3.1.0; extra == "docs"
71
71
  Requires-Dist: mkdocs-gen-files==0.5.0; extra == "docs"
@@ -35,5 +35,3 @@ from .commons import (
35
35
  results,
36
36
  solvers,
37
37
  )
38
-
39
- CONFIG.load_config()
@@ -91,13 +91,13 @@ class Calculation:
91
91
  model.label_of_element: float(model.size.solution)
92
92
  for component in self.flow_system.components.values()
93
93
  for model in component.model.all_sub_models
94
- if isinstance(model, InvestmentModel) and float(model.size.solution) >= CONFIG.modeling.EPSILON
94
+ if isinstance(model, InvestmentModel) and float(model.size.solution) >= CONFIG.Modeling.epsilon
95
95
  },
96
96
  'Not invested': {
97
97
  model.label_of_element: float(model.size.solution)
98
98
  for component in self.flow_system.components.values()
99
99
  for model in component.model.all_sub_models
100
- if isinstance(model, InvestmentModel) and float(model.size.solution) < CONFIG.modeling.EPSILON
100
+ if isinstance(model, InvestmentModel) and float(model.size.solution) < CONFIG.Modeling.epsilon
101
101
  },
102
102
  },
103
103
  'Buses with excess': [
@@ -0,0 +1,561 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import warnings
5
+ from logging.handlers import RotatingFileHandler
6
+ from pathlib import Path
7
+ from types import MappingProxyType
8
+ from typing import Literal
9
+
10
+ import yaml
11
+ from rich.console import Console
12
+ from rich.logging import RichHandler
13
+ from rich.style import Style
14
+ from rich.theme import Theme
15
+
16
+ __all__ = ['CONFIG', 'change_logging_level']
17
+
18
+ logger = logging.getLogger('flixopt')
19
+
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
+ )
57
+
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::
187
+
188
+ CONFIG.reset()
189
+
190
+ Export current configuration::
191
+
192
+ config_dict = CONFIG.to_dict()
193
+ import yaml
194
+
195
+ with open('my_config.yaml', 'w') as f:
196
+ yaml.dump(config_dict, f)
197
+ """
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']
224
+
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()
241
+
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
+ )
267
+
268
+ @classmethod
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}')
274
+
275
+ with config_path.open() as file:
276
+ config_dict = yaml.safe_load(file)
277
+ cls._apply_config_dict(config_dict)
278
+
279
+ cls.apply()
280
+
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)
298
+
299
+ @classmethod
300
+ def to_dict(cls):
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
+ }
329
+
330
+
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
+
337
+ def format(self, record):
338
+ message_lines = record.getMessage().split('\n')
339
+ timestamp = self.formatTime(record, self.datefmt)
340
+ log_level = record.levelname.ljust(8)
341
+ log_prefix = f'{timestamp} | {log_level} |'
342
+
343
+ first_line = [f'{log_prefix} {message_lines[0]}']
344
+ if len(message_lines) > 1:
345
+ lines = first_line + [f'{log_prefix} {line}' for line in message_lines[1:]]
346
+ else:
347
+ lines = first_line
348
+
349
+ return '\n'.join(lines)
350
+
351
+
352
+ class ColoredMultilineFormater(MultilineFormater):
353
+ """Formatter that adds ANSI colors to multi-line log messages."""
354
+
355
+ RESET = '\033[0m'
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
+
371
+ def format(self, record):
372
+ lines = super().format(record).splitlines()
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)
376
+
377
+
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.
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.
395
+
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(
418
+ console=console,
419
+ rich_tracebacks=True,
420
+ omit_repeated_times=True,
421
+ show_path=show_path,
422
+ log_time_format=date_format,
423
+ )
424
+ handler.setFormatter(logging.Formatter(format))
425
+ else:
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.
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
459
+
460
+
461
+ def _setup_logging(
462
+ default_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO',
463
+ log_file: str | None = None,
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,
473
+ ):
474
+ """Internal function to setup logging - use CONFIG.apply() instead.
475
+
476
+ Configures the flixopt logger with console and/or file handlers.
477
+ If no handlers are configured, adds NullHandler (library best practice).
478
+
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
+ )
519
+
520
+ # Library best practice: NullHandler if no handlers configured
521
+ if not logger.handlers:
522
+ logger.addHandler(logging.NullHandler())
523
+
524
+ return logger
525
+
526
+
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
+ )
553
+ logger = logging.getLogger('flixopt')
554
+ logging_level = getattr(logging, level_name.upper())
555
+ logger.setLevel(logging_level)
556
+ for handler in logger.handlers:
557
+ handler.setLevel(logging_level)
558
+
559
+
560
+ # Initialize default config
561
+ CONFIG.apply()
@@ -248,7 +248,7 @@ class Flow(Element):
248
248
  size: Flow capacity or nominal rating. Can be:
249
249
  - Scalar value for fixed capacity
250
250
  - InvestParameters for investment-based sizing decisions
251
- - None to use large default value (CONFIG.modeling.BIG)
251
+ - None to use large default value (CONFIG.Modeling.big)
252
252
  relative_minimum: Minimum flow rate as fraction of size.
253
253
  Example: 0.2 means flow cannot go below 20% of rated capacity.
254
254
  relative_maximum: Maximum flow rate as fraction of size (typically 1.0).
@@ -356,7 +356,7 @@ class Flow(Element):
356
356
  `relative_maximum` for upper bounds on optimization variables.
357
357
 
358
358
  Notes:
359
- - Default size (CONFIG.modeling.BIG) is used when size=None
359
+ - Default size (CONFIG.Modeling.big) is used when size=None
360
360
  - list inputs for previous_flow_rate are converted to NumPy arrays
361
361
  - Flow direction is determined by component input/output designation
362
362
 
@@ -383,7 +383,7 @@ class Flow(Element):
383
383
  meta_data: dict | None = None,
384
384
  ):
385
385
  super().__init__(label, meta_data=meta_data)
386
- self.size = CONFIG.modeling.BIG if size is None else size
386
+ self.size = CONFIG.Modeling.big if size is None else size
387
387
  self.relative_minimum = relative_minimum
388
388
  self.relative_maximum = relative_maximum
389
389
  self.fixed_relative_profile = fixed_relative_profile
@@ -455,11 +455,11 @@ class Flow(Element):
455
455
  raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!')
456
456
 
457
457
  if (
458
- self.size == CONFIG.modeling.BIG and self.fixed_relative_profile is not None
458
+ self.size == CONFIG.Modeling.big and self.fixed_relative_profile is not None
459
459
  ): # Default Size --> Most likely by accident
460
460
  logger.warning(
461
461
  f'Flow "{self.label}" has no size assigned, but a "fixed_relative_profile". '
462
- f'The default size is {CONFIG.modeling.BIG}. As "flow_rate = size * fixed_relative_profile", '
462
+ f'The default size is {CONFIG.Modeling.big}. As "flow_rate = size * fixed_relative_profile", '
463
463
  f'the resulting flow_rate will be very high. To fix this, assign a size to the Flow {self}.'
464
464
  )
465
465
 
@@ -143,7 +143,7 @@ class InvestmentModel(Model):
143
143
  # eq2: P_invest >= isInvested * max(epsilon, investSize_min)
144
144
  self.add(
145
145
  self._model.add_constraints(
146
- self.size >= self.is_invested * np.maximum(CONFIG.modeling.EPSILON, self.parameters.minimum_size),
146
+ self.size >= self.is_invested * np.maximum(CONFIG.Modeling.epsilon, self.parameters.minimum_size),
147
147
  name=f'{self.label_full}|is_invested_lb',
148
148
  ),
149
149
  'is_invested_lb',
@@ -304,7 +304,7 @@ class StateModel(Model):
304
304
  # Constraint: on * lower_bound <= def_var
305
305
  self.add(
306
306
  self._model.add_constraints(
307
- self.on * np.maximum(CONFIG.modeling.EPSILON, lb) <= def_var, name=f'{self.label_full}|on_con1'
307
+ self.on * np.maximum(CONFIG.Modeling.epsilon, lb) <= def_var, name=f'{self.label_full}|on_con1'
308
308
  ),
309
309
  'on_con1',
310
310
  )
@@ -314,7 +314,7 @@ class StateModel(Model):
314
314
  else:
315
315
  # Case for multiple defining variables
316
316
  ub = sum(bound[1] for bound in self._defining_bounds) / nr_of_def_vars
317
- lb = CONFIG.modeling.EPSILON # TODO: Can this be a bigger value? (maybe the smallest bound?)
317
+ lb = CONFIG.Modeling.epsilon # TODO: Can this be a bigger value? (maybe the smallest bound?)
318
318
 
319
319
  # Constraint: on * epsilon <= sum(all_defining_variables)
320
320
  self.add(
@@ -337,7 +337,7 @@ class StateModel(Model):
337
337
  @property
338
338
  def previous_states(self) -> np.ndarray:
339
339
  """Computes the previous states {0, 1} of defining variables as a binary array from their previous values."""
340
- return StateModel.compute_previous_states(self._previous_values, epsilon=CONFIG.modeling.EPSILON)
340
+ return StateModel.compute_previous_states(self._previous_values, epsilon=CONFIG.Modeling.epsilon)
341
341
 
342
342
  @property
343
343
  def previous_on_states(self) -> np.ndarray:
@@ -603,14 +603,14 @@ class ConsecutiveStateModel(Model):
603
603
  elif np.isscalar(binary_values) and not np.isscalar(hours_per_timestep):
604
604
  return binary_values * hours_per_timestep[-1]
605
605
 
606
- if np.isclose(binary_values[-1], 0, atol=CONFIG.modeling.EPSILON):
606
+ if np.isclose(binary_values[-1], 0, atol=CONFIG.Modeling.epsilon):
607
607
  return 0
608
608
 
609
609
  if np.isscalar(hours_per_timestep):
610
610
  hours_per_timestep = np.ones(len(binary_values)) * hours_per_timestep
611
611
  hours_per_timestep: np.ndarray
612
612
 
613
- indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.modeling.EPSILON))[0]
613
+ indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.Modeling.epsilon))[0]
614
614
  if len(indexes_with_zero_values) == 0:
615
615
  nr_of_indexes_with_consecutive_ones = len(binary_values)
616
616
  else:
@@ -650,10 +650,10 @@ class InvestParameters(Interface):
650
650
  fixed_size: When specified, creates a binary investment decision at exactly
651
651
  this size. When None, allows continuous sizing between minimum and maximum bounds.
652
652
  minimum_size: Lower bound for continuous sizing decisions. Defaults to a small
653
- positive value (CONFIG.modeling.EPSILON) to avoid numerical issues.
653
+ positive value (CONFIG.Modeling.epsilon) to avoid numerical issues.
654
654
  Ignored when fixed_size is specified.
655
655
  maximum_size: Upper bound for continuous sizing decisions. Defaults to a large
656
- value (CONFIG.modeling.BIG) representing unlimited capacity.
656
+ value (CONFIG.Modeling.big) representing unlimited capacity.
657
657
  Ignored when fixed_size is specified.
658
658
  optional: Controls whether investment is required. When True (default),
659
659
  optimization can choose not to invest. When False, forces investment
@@ -833,8 +833,8 @@ class InvestParameters(Interface):
833
833
  self.optional = optional
834
834
  self.specific_effects: EffectValuesUserScalar = specific_effects or {}
835
835
  self.piecewise_effects = piecewise_effects
836
- self._minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON
837
- self._maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG # default maximum
836
+ self._minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon
837
+ self._maximum_size = maximum_size if maximum_size is not None else CONFIG.Modeling.big # default maximum
838
838
 
839
839
  def transform_data(self, flow_system: FlowSystem):
840
840
  self.fix_effects = flow_system.effects.create_effect_values_dict(self.fix_effects)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flixopt
3
- Version: 2.1.11
3
+ Version: 2.2.0
4
4
  Summary: Vector based energy and material flow optimization framework in Python.
5
5
  Author-email: "Chair of Building Energy Systems and Heat Supply, TU Dresden" <peter.stange@tu-dresden.de>, Felix Bumann <felixbumann387@gmail.com>, Felix Panitz <baumbude@googlemail.com>, Peter Stange <peter.stange@tu-dresden.de>
6
6
  Maintainer-email: Felix Bumann <felixbumann387@gmail.com>, Peter Stange <peter.stange@tu-dresden.de>
@@ -53,10 +53,10 @@ Provides-Extra: dev
53
53
  Requires-Dist: pytest==8.4.2; extra == "dev"
54
54
  Requires-Dist: pytest-xdist==3.8.0; extra == "dev"
55
55
  Requires-Dist: nbformat==5.10.4; extra == "dev"
56
- Requires-Dist: ruff==0.13.0; extra == "dev"
56
+ Requires-Dist: ruff==0.13.2; extra == "dev"
57
57
  Requires-Dist: pre-commit==4.3.0; extra == "dev"
58
58
  Requires-Dist: pyvis==0.3.2; extra == "dev"
59
- Requires-Dist: tsam==2.3.1; extra == "dev"
59
+ Requires-Dist: tsam==2.3.9; extra == "dev"
60
60
  Requires-Dist: scipy==1.15.1; extra == "dev"
61
61
  Requires-Dist: gurobipy==12.0.3; extra == "dev"
62
62
  Requires-Dist: dash==3.0.0; extra == "dev"
@@ -65,7 +65,7 @@ Requires-Dist: dash-daq==0.6.0; extra == "dev"
65
65
  Requires-Dist: networkx==3.0.0; extra == "dev"
66
66
  Requires-Dist: werkzeug==3.0.0; extra == "dev"
67
67
  Provides-Extra: docs
68
- Requires-Dist: mkdocs-material==9.6.19; extra == "docs"
68
+ Requires-Dist: mkdocs-material==9.6.20; extra == "docs"
69
69
  Requires-Dist: mkdocstrings-python==1.18.2; extra == "docs"
70
70
  Requires-Dist: mkdocs-table-reader-plugin==3.1.0; extra == "docs"
71
71
  Requires-Dist: mkdocs-gen-files==0.5.0; extra == "docs"
@@ -9,7 +9,6 @@ flixopt/calculation.py
9
9
  flixopt/commons.py
10
10
  flixopt/components.py
11
11
  flixopt/config.py
12
- flixopt/config.yaml
13
12
  flixopt/core.py
14
13
  flixopt/effects.py
15
14
  flixopt/elements.py
@@ -16,10 +16,10 @@ tomli<3,>=2.0.1
16
16
  pytest==8.4.2
17
17
  pytest-xdist==3.8.0
18
18
  nbformat==5.10.4
19
- ruff==0.13.0
19
+ ruff==0.13.2
20
20
  pre-commit==4.3.0
21
21
  pyvis==0.3.2
22
- tsam==2.3.1
22
+ tsam==2.3.9
23
23
  scipy==1.15.1
24
24
  gurobipy==12.0.3
25
25
  dash==3.0.0
@@ -29,7 +29,7 @@ networkx==3.0.0
29
29
  werkzeug==3.0.0
30
30
 
31
31
  [docs]
32
- mkdocs-material==9.6.19
32
+ mkdocs-material==9.6.20
33
33
  mkdocstrings-python==1.18.2
34
34
  mkdocs-table-reader-plugin==3.1.0
35
35
  mkdocs-gen-files==0.5.0
@@ -80,10 +80,10 @@ dev = [
80
80
  "pytest==8.4.2",
81
81
  "pytest-xdist==3.8.0",
82
82
  "nbformat==5.10.4",
83
- "ruff==0.13.0",
83
+ "ruff==0.13.2",
84
84
  "pre-commit==4.3.0",
85
85
  "pyvis==0.3.2",
86
- "tsam==2.3.1",
86
+ "tsam==2.3.9",
87
87
  "scipy==1.15.1",
88
88
  "gurobipy==12.0.3",
89
89
  "dash==3.0.0",
@@ -95,7 +95,7 @@ dev = [
95
95
 
96
96
  # Documentation building
97
97
  docs = [
98
- "mkdocs-material==9.6.19",
98
+ "mkdocs-material==9.6.20",
99
99
  "mkdocstrings-python==1.18.2",
100
100
  "mkdocs-table-reader-plugin==3.1.0",
101
101
  "mkdocs-gen-files==0.5.0",
@@ -1,268 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- import os
5
- import types
6
- from dataclasses import dataclass, fields, is_dataclass
7
- from typing import Annotated, Literal, get_type_hints
8
-
9
- import yaml
10
- from rich.console import Console
11
- from rich.logging import RichHandler
12
-
13
- logger = logging.getLogger('flixopt')
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)
60
-
61
-
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
-
73
-
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
-
83
-
84
- @dataclass
85
- class ModelingConfig(ValidatedConfig):
86
- BIG: Annotated[int, lambda x: isinstance(x, int)]
87
- EPSILON: Annotated[float, lambda x: isinstance(x, float)]
88
- BIG_BINARY_BOUND: Annotated[int, lambda x: isinstance(x, int)]
89
-
90
-
91
- @dataclass
92
- class ConfigSchema(ValidatedConfig):
93
- config_name: Annotated[str, lambda x: isinstance(x, str)]
94
- logging: LoggingConfig
95
- modeling: ModelingConfig
96
-
97
-
98
- class CONFIG:
99
- """
100
- A configuration class that stores global configuration values as class attributes.
101
- """
102
-
103
- config_name: str = None
104
- modeling: ModelingConfig = None
105
- logging: LoggingConfig = None
106
-
107
- @classmethod
108
- def load_config(cls, user_config_file: str | None = None):
109
- """
110
- Initialize configuration using defaults or user-specified file.
111
- """
112
- # Default config file
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)
123
-
124
- # Convert the merged config to ConfigSchema
125
- config_data = dataclass_from_dict_with_validation(ConfigSchema, new_config)
126
-
127
- # Store the configuration in the class as class attributes
128
- cls.logging = config_data.logging
129
- cls.modeling = config_data.modeling
130
- cls.config_name = config_data.config_name
131
-
132
- setup_logging(default_level=cls.logging.level, log_file=cls.logging.file, use_rich_handler=cls.logging.rich)
133
-
134
- @classmethod
135
- def to_dict(cls):
136
- """
137
- Convert the configuration class into a dictionary for JSON serialization.
138
- Handles dataclasses and simple types like str, int, etc.
139
- """
140
- config_dict = {}
141
- for attribute, value in cls.__dict__.items():
142
- # Only consider attributes (not methods, etc.)
143
- if (
144
- not attribute.startswith('_')
145
- and not isinstance(value, (types.FunctionType, types.MethodType))
146
- and not isinstance(value, classmethod)
147
- ):
148
- if is_dataclass(value):
149
- config_dict[attribute] = value.__dict__
150
- else: # Assuming only basic types here!
151
- config_dict[attribute] = value
152
-
153
- return config_dict
154
-
155
-
156
- class MultilineFormater(logging.Formatter):
157
- def format(self, record):
158
- message_lines = record.getMessage().split('\n')
159
-
160
- # Prepare the log prefix (timestamp + log level)
161
- timestamp = self.formatTime(record, self.datefmt)
162
- log_level = record.levelname.ljust(8) # Align log levels for consistency
163
- log_prefix = f'{timestamp} | {log_level} |'
164
-
165
- # Format all lines
166
- first_line = [f'{log_prefix} {message_lines[0]}']
167
- if len(message_lines) > 1:
168
- lines = first_line + [f'{log_prefix} {line}' for line in message_lines[1:]]
169
- else:
170
- lines = first_line
171
-
172
- return '\n'.join(lines)
173
-
174
-
175
- class ColoredMultilineFormater(MultilineFormater):
176
- # ANSI escape codes for colors
177
- COLORS = {
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
- }
184
- RESET = '\033[0m'
185
-
186
- def format(self, record):
187
- lines = super().format(record).splitlines()
188
- log_color = self.COLORS.get(record.levelname, self.RESET)
189
-
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
-
195
- return '\n'.join(formatted_lines)
196
-
197
-
198
- def _get_logging_handler(log_file: str | None = None, use_rich_handler: bool = False) -> logging.Handler:
199
- """Returns a logging handler for the given log file."""
200
- if use_rich_handler and log_file is None:
201
- # RichHandler for console output
202
- console = Console(width=120)
203
- rich_handler = RichHandler(
204
- console=console,
205
- rich_tracebacks=True,
206
- omit_repeated_times=True,
207
- show_path=False,
208
- log_time_format='%Y-%m-%d %H:%M:%S',
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
- )
221
- )
222
- return file_handler
223
- else:
224
- # FileHandler for file output
225
- file_handler = logging.FileHandler(log_file)
226
- file_handler.setFormatter(
227
- MultilineFormater(
228
- fmt='%(message)s',
229
- datefmt='%Y-%m-%d %H:%M:%S',
230
- )
231
- )
232
- return file_handler
233
-
234
-
235
- def setup_logging(
236
- default_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO',
237
- log_file: str | None = 'flixopt.log',
238
- use_rich_handler: bool = False,
239
- ):
240
- """Setup logging configuration"""
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()
246
-
247
- logger.addHandler(_get_logging_handler(use_rich_handler=use_rich_handler))
248
- if log_file is not None:
249
- logger.addHandler(_get_logging_handler(log_file, use_rich_handler=False))
250
-
251
- return logger
252
-
253
-
254
- def get_logging_level_by_name(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']) -> int:
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
261
-
262
-
263
- def change_logging_level(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']):
264
- logger = logging.getLogger('flixopt')
265
- logging_level = get_logging_level_by_name(level_name)
266
- logger.setLevel(logging_level)
267
- for handler in logger.handlers:
268
- handler.setLevel(logging_level)
@@ -1,10 +0,0 @@
1
- # Default configuration of flixopt
2
- config_name: flixopt # Name of the config file. This has no effect on the configuration itself.
3
- logging:
4
- level: INFO
5
- file: flixopt.log
6
- rich: false # logging output is formatted using rich. This is only advisable when using a proper terminal
7
- modeling:
8
- BIG: 10000000 # 1e notation not possible in yaml
9
- EPSILON: 0.00001
10
- BIG_BINARY_BOUND: 100000
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes