flixopt 2.2.0rc2__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.

Files changed (58) hide show
  1. flixopt/__init__.py +33 -4
  2. flixopt/aggregation.py +60 -80
  3. flixopt/calculation.py +395 -178
  4. flixopt/commons.py +1 -10
  5. flixopt/components.py +939 -448
  6. flixopt/config.py +553 -191
  7. flixopt/core.py +513 -846
  8. flixopt/effects.py +644 -178
  9. flixopt/elements.py +610 -355
  10. flixopt/features.py +394 -966
  11. flixopt/flow_system.py +736 -219
  12. flixopt/interface.py +1104 -302
  13. flixopt/io.py +103 -79
  14. flixopt/linear_converters.py +387 -95
  15. flixopt/modeling.py +759 -0
  16. flixopt/network_app.py +73 -39
  17. flixopt/plotting.py +294 -138
  18. flixopt/results.py +1253 -299
  19. flixopt/solvers.py +25 -21
  20. flixopt/structure.py +938 -396
  21. flixopt/utils.py +38 -12
  22. flixopt-3.0.0.dist-info/METADATA +209 -0
  23. flixopt-3.0.0.dist-info/RECORD +26 -0
  24. flixopt-3.0.0.dist-info/top_level.txt +1 -0
  25. docs/examples/00-Minimal Example.md +0 -5
  26. docs/examples/01-Basic Example.md +0 -5
  27. docs/examples/02-Complex Example.md +0 -10
  28. docs/examples/03-Calculation Modes.md +0 -5
  29. docs/examples/index.md +0 -5
  30. docs/faq/contribute.md +0 -61
  31. docs/faq/index.md +0 -3
  32. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  33. docs/images/architecture_flixOpt.png +0 -0
  34. docs/images/flixopt-icon.svg +0 -1
  35. docs/javascripts/mathjax.js +0 -18
  36. docs/user-guide/Mathematical Notation/Bus.md +0 -33
  37. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
  38. docs/user-guide/Mathematical Notation/Flow.md +0 -26
  39. docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
  40. docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
  41. docs/user-guide/Mathematical Notation/Storage.md +0 -44
  42. docs/user-guide/Mathematical Notation/index.md +0 -22
  43. docs/user-guide/Mathematical Notation/others.md +0 -3
  44. docs/user-guide/index.md +0 -124
  45. flixopt/config.yaml +0 -10
  46. flixopt-2.2.0rc2.dist-info/METADATA +0 -167
  47. flixopt-2.2.0rc2.dist-info/RECORD +0 -54
  48. flixopt-2.2.0rc2.dist-info/top_level.txt +0 -5
  49. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  50. pics/architecture_flixOpt.png +0 -0
  51. pics/flixOpt_plotting.jpg +0 -0
  52. pics/flixopt-icon.svg +0 -1
  53. pics/pics.pptx +0 -0
  54. scripts/extract_release_notes.py +0 -45
  55. scripts/gen_ref_pages.py +0 -54
  56. tests/ressources/Zeitreihen2020.csv +0 -35137
  57. {flixopt-2.2.0rc2.dist-info → flixopt-3.0.0.dist-info}/WHEEL +0 -0
  58. {flixopt-2.2.0rc2.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 os
3
- import types
4
- from dataclasses import dataclass, fields, is_dataclass
5
- from typing import Annotated, Literal, Optional
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
- def merge_configs(defaults: dict, overrides: dict) -> dict:
15
- """
16
- Merge the default configuration with user-provided overrides.
17
- Args:
18
- defaults: Default configuration dictionary.
19
- overrides: User configuration dictionary.
20
- Returns:
21
- Merged configuration dictionary.
22
- """
23
- for key, value in overrides.items():
24
- if isinstance(value, dict) and key in defaults and isinstance(defaults[key], dict):
25
- # Recursively merge nested dictionaries
26
- defaults[key] = merge_configs(defaults[key], value)
27
- else:
28
- # Override the default value
29
- defaults[key] = value
30
- return defaults
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
- def dataclass_from_dict_with_validation(cls, data: dict):
34
- """
35
- Recursively initialize a dataclass from a dictionary.
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
- return cls(**kwargs)
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
- @dataclass()
57
- class ValidatedConfig:
58
- def __setattr__(self, name, value):
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
- @dataclass
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)]
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
- @dataclass
76
- class ModelingConfig(ValidatedConfig):
77
- BIG: Annotated[int, lambda x: isinstance(x, int)]
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
- @dataclass
83
- class ConfigSchema(ValidatedConfig):
84
- config_name: Annotated[str, lambda x: isinstance(x, str)]
85
- logging: LoggingConfig
86
- modeling: ModelingConfig
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
- class CONFIG:
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 load_config(cls, user_config_file: Optional[str] = None):
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
- # Store the configuration in the class as class attributes
119
- cls.logging = config_data.logging
120
- cls.modeling = config_data.modeling
121
- cls.config_name = config_data.config_name
122
-
123
- setup_logging(default_level=cls.logging.level, log_file=cls.logging.file, use_rich_handler=cls.logging.rich)
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
- 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')
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
- # Prepare the log prefix (timestamp + log level)
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) # Align log levels for consistency
154
- log_prefix = f'{timestamp} | {log_level} |'
342
+ log_level = record.levelname.ljust(8)
155
343
 
156
- # Format all lines
157
- first_line = [f'{log_prefix} {message_lines[0]}']
158
- if len(message_lines) > 1:
159
- lines = first_line + [f'{log_prefix} {line}' for line in message_lines[1:]]
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
- lines = first_line
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 ColoredMultilineFormater(MultilineFormater):
167
- # ANSI escape codes for colors
168
- COLORS = {
169
- 'DEBUG': '\033[32m', # Green
170
- 'INFO': '\033[34m', # Blue
171
- 'WARNING': '\033[33m', # Yellow
172
- 'ERROR': '\033[31m', # Red
173
- 'CRITICAL': '\033[1m\033[31m', # Bold Red
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
- return '\n'.join(formatted_lines)
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
- def _get_logging_handler(log_file: Optional[str] = None, use_rich_handler: bool = False) -> logging.Handler:
190
- """Returns a logging handler for the given log file."""
191
- if use_rich_handler and log_file is None:
192
- # RichHandler for console output
193
- console = Console(width=120)
194
- rich_handler = RichHandler(
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=False,
199
- log_time_format='%Y-%m-%d %H:%M:%S',
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
- return file_handler
452
+ handler.setFormatter(logging.Formatter(format))
214
453
  else:
215
- # FileHandler for file output
216
- file_handler = logging.FileHandler(log_file)
217
- file_handler.setFormatter(
218
- MultilineFormater(
219
- fmt='%(message)s',
220
- datefmt='%Y-%m-%d %H:%M:%S',
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
- def setup_logging(
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: Optional[str] = 'flixopt.log',
521
+ log_file: str | None = None,
229
522
  use_rich_handler: bool = False,
230
- ):
231
- """Setup logging configuration"""
232
- logger = logging.getLogger('flixopt') # Use a specific logger name for your package
233
- logger.setLevel(get_logging_level_by_name(default_level))
234
- # Clear existing handlers
235
- if logger.hasHandlers():
236
- logger.handlers.clear()
237
-
238
- logger.addHandler(_get_logging_handler(use_rich_handler=use_rich_handler))
239
- if log_file is not None:
240
- logger.addHandler(_get_logging_handler(log_file, use_rich_handler=False))
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
- return logger
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
- def get_logging_level_by_name(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']) -> int:
246
- possible_logging_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
247
- if level_name.upper() not in possible_logging_levels:
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 = get_logging_level_by_name(level_name)
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()