dfm-python 0.2.4__py3-none-any.whl → 0.2.6__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.
- dfm_python/__init__.py +10 -53
- dfm_python/api.py +4 -4
- dfm_python/config.py +228 -554
- dfm_python/config_sources.py +558 -0
- dfm_python/core/__init__.py +2 -57
- dfm_python/core/diagnostics.py +162 -5
- dfm_python/core/em.py +1053 -875
- dfm_python/core/helpers.py +977 -35
- dfm_python/core/numeric.py +392 -407
- dfm_python/data.py +609 -0
- dfm_python/dfm.py +285 -246
- dfm_python/kalman.py +29 -26
- dfm_python/news.py +1 -1
- dfm_python/{utils/aggregation.py → utils.py} +148 -2
- dfm_python-0.2.6.dist-info/METADATA +792 -0
- dfm_python-0.2.6.dist-info/RECORD +18 -0
- dfm_python/core/grouping.py +0 -54
- dfm_python/core/results.py +0 -70
- dfm_python/data_loader.py +0 -798
- dfm_python/utils/__init__.py +0 -25
- dfm_python/utils/data_utils.py +0 -231
- dfm_python-0.2.4.dist-info/METADATA +0 -355
- dfm_python-0.2.4.dist-info/RECORD +0 -21
- {dfm_python-0.2.4.dist-info → dfm_python-0.2.6.dist-info}/WHEEL +0 -0
- {dfm_python-0.2.4.dist-info → dfm_python-0.2.6.dist-info}/licenses/LICENSE +0 -0
dfm_python/config.py
CHANGED
|
@@ -1,48 +1,35 @@
|
|
|
1
|
-
"""Configuration models and
|
|
1
|
+
"""Configuration models and factory methods for DFM nowcasting.
|
|
2
2
|
|
|
3
|
-
This module provides
|
|
3
|
+
This module provides the core configuration system for Dynamic Factor Models:
|
|
4
4
|
- Configuration dataclasses (DFMConfig, SeriesConfig, BlockConfig, Params)
|
|
5
|
-
-
|
|
6
|
-
- Factory functions for flexible config loading
|
|
5
|
+
- Factory methods for creating DFMConfig from dictionaries and Hydra configs
|
|
7
6
|
|
|
8
|
-
The configuration
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- Dictionary configurations
|
|
13
|
-
- Merging multiple configuration sources
|
|
7
|
+
The configuration dataclasses define:
|
|
8
|
+
- Model structure (series, blocks, factors)
|
|
9
|
+
- Estimation parameters (EM algorithm settings)
|
|
10
|
+
- Numerical stability controls (regularization, clipping, damping)
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
Factory methods support:
|
|
13
|
+
- Dictionary configurations (legacy format, new format, Hydra format)
|
|
14
|
+
- Hydra DictConfig objects (via from_hydra())
|
|
15
|
+
|
|
16
|
+
For loading configurations from files (YAML, Spec CSV) or other sources,
|
|
17
|
+
see the config_sources module which provides source adapters.
|
|
18
|
+
|
|
19
|
+
Note: Source adapter classes (YamlSource, DictSource, etc.) are re-exported
|
|
20
|
+
from config_sources for backward compatibility.
|
|
17
21
|
"""
|
|
18
22
|
|
|
19
23
|
import numpy as np
|
|
20
24
|
from typing import List, Optional, Dict, Any, Union
|
|
21
|
-
from
|
|
22
|
-
import warnings
|
|
23
|
-
import logging
|
|
24
|
-
from dataclasses import dataclass, field, is_dataclass, asdict
|
|
25
|
+
from dataclasses import dataclass, field
|
|
25
26
|
|
|
26
27
|
try:
|
|
27
28
|
from typing import Protocol
|
|
28
29
|
except ImportError:
|
|
29
30
|
from typing_extensions import Protocol
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
from hydra.core.config_store import ConfigStore
|
|
33
|
-
HYDRA_AVAILABLE = True
|
|
34
|
-
except ImportError:
|
|
35
|
-
HYDRA_AVAILABLE = False
|
|
36
|
-
ConfigStore = None
|
|
37
|
-
|
|
38
|
-
# Valid frequency codes
|
|
39
|
-
_VALID_FREQUENCIES = {'d', 'w', 'm', 'q', 'sa', 'a'}
|
|
40
|
-
|
|
41
|
-
# Valid transformation codes
|
|
42
|
-
_VALID_TRANSFORMATIONS = {
|
|
43
|
-
'lin', 'chg', 'ch1', 'pch', 'pc1', 'pca',
|
|
44
|
-
'cch', 'cca', 'log'
|
|
45
|
-
}
|
|
32
|
+
# Validation functions are defined at the end of this file
|
|
46
33
|
|
|
47
34
|
# Default global block name (can be overridden in config)
|
|
48
35
|
DEFAULT_GLOBAL_BLOCK_NAME = 'Block_Global'
|
|
@@ -61,20 +48,6 @@ _TRANSFORM_UNITS_MAP = {
|
|
|
61
48
|
}
|
|
62
49
|
|
|
63
50
|
|
|
64
|
-
def validate_frequency(frequency: str) -> str:
|
|
65
|
-
"""Validate frequency code."""
|
|
66
|
-
if frequency not in _VALID_FREQUENCIES:
|
|
67
|
-
raise ValueError(f"Invalid frequency: {frequency}. Must be one of {_VALID_FREQUENCIES}")
|
|
68
|
-
return frequency
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def validate_transformation(transformation: str) -> str:
|
|
72
|
-
"""Validate transformation code."""
|
|
73
|
-
if transformation not in _VALID_TRANSFORMATIONS:
|
|
74
|
-
warnings.warn(f"Unknown transformation code: {transformation}. Will use untransformed data.")
|
|
75
|
-
return transformation
|
|
76
|
-
|
|
77
|
-
|
|
78
51
|
@dataclass
|
|
79
52
|
class BlockConfig:
|
|
80
53
|
"""Configuration for a single factor block.
|
|
@@ -262,6 +235,14 @@ class Params:
|
|
|
262
235
|
Damping factor: 0.8 = 80% new, 20% old (default: 0.8)
|
|
263
236
|
warn_on_damped_update : bool
|
|
264
237
|
Warn when damped updates are used (default: True)
|
|
238
|
+
augment_idio : bool
|
|
239
|
+
Enable state augmentation with idiosyncratic components (default: True)
|
|
240
|
+
augment_idio_slow : bool
|
|
241
|
+
Enable tent-length chains for slower-frequency series (default: True)
|
|
242
|
+
idio_rho0 : float
|
|
243
|
+
Initial AR coefficient for idiosyncratic components (default: 0.1)
|
|
244
|
+
idio_min_var : float
|
|
245
|
+
Minimum variance for idiosyncratic innovation covariance (default: 1e-8)
|
|
265
246
|
|
|
266
247
|
Examples
|
|
267
248
|
--------
|
|
@@ -300,6 +281,12 @@ class Params:
|
|
|
300
281
|
damping_factor: float = 0.8
|
|
301
282
|
warn_on_damped_update: bool = True
|
|
302
283
|
|
|
284
|
+
# Idiosyncratic component augmentation
|
|
285
|
+
augment_idio: bool = True # Enable state augmentation with idiosyncratic components (default: True)
|
|
286
|
+
augment_idio_slow: bool = True # Enable tent-length chains for slower-frequency series (default: True)
|
|
287
|
+
idio_rho0: float = 0.1 # Initial AR coefficient for idiosyncratic components (default: 0.1)
|
|
288
|
+
idio_min_var: float = 1e-8 # Minimum variance for idiosyncratic innovation covariance (default: 1e-8)
|
|
289
|
+
|
|
303
290
|
def __post_init__(self):
|
|
304
291
|
"""Validate parameters."""
|
|
305
292
|
self.clock = validate_frequency(self.clock)
|
|
@@ -373,6 +360,12 @@ class DFMConfig:
|
|
|
373
360
|
damping_factor: float = 0.8 # Damping factor (0.8 = 80% new, 20% old)
|
|
374
361
|
warn_on_damped_update: bool = True # Warn when damped updates are used
|
|
375
362
|
|
|
363
|
+
# Idiosyncratic Component Augmentation
|
|
364
|
+
augment_idio: bool = True # Enable state augmentation with idiosyncratic components (default: True)
|
|
365
|
+
augment_idio_slow: bool = True # Enable tent-length chains for slower-frequency series (default: True)
|
|
366
|
+
idio_rho0: float = 0.1 # Initial AR coefficient for idiosyncratic components (default: 0.1)
|
|
367
|
+
idio_min_var: float = 1e-8 # Minimum variance for idiosyncratic innovation covariance (default: 1e-8)
|
|
368
|
+
|
|
376
369
|
# ========================================================================
|
|
377
370
|
# Internal cache (not user-configurable)
|
|
378
371
|
# ========================================================================
|
|
@@ -397,7 +390,7 @@ class DFMConfig:
|
|
|
397
390
|
indicating what needs to be fixed.
|
|
398
391
|
"""
|
|
399
392
|
# Import frequency hierarchy for validation
|
|
400
|
-
from .utils
|
|
393
|
+
from .utils import FREQUENCY_HIERARCHY
|
|
401
394
|
|
|
402
395
|
if not self.series:
|
|
403
396
|
raise ValueError(
|
|
@@ -446,7 +439,9 @@ class DFMConfig:
|
|
|
446
439
|
if block_clock_hierarchy < global_clock_hierarchy:
|
|
447
440
|
raise ValueError(
|
|
448
441
|
f"Block '{block_name}' has clock '{block_cfg.clock}' which is faster than "
|
|
449
|
-
f"global clock '{self.clock}'. Block clocks must be >= global clock."
|
|
442
|
+
f"global clock '{self.clock}'. Block clocks must be >= global clock. "
|
|
443
|
+
f"Suggested fix: change block '{block_name}' clock to '{self.clock}' or slower, "
|
|
444
|
+
f"or set global clock to '{block_cfg.clock}' or faster."
|
|
450
445
|
)
|
|
451
446
|
|
|
452
447
|
# Auto-generate series_id if not provided and convert blocks to indices
|
|
@@ -496,11 +491,17 @@ class DFMConfig:
|
|
|
496
491
|
|
|
497
492
|
# Series frequency must be <= block clock (slower or equal)
|
|
498
493
|
if series_freq_hierarchy < block_clock_hierarchy:
|
|
494
|
+
# Suggest valid frequencies for the series
|
|
495
|
+
valid_freqs = [freq for freq, hier in FREQUENCY_HIERARCHY.items()
|
|
496
|
+
if hier >= block_clock_hierarchy]
|
|
497
|
+
valid_freqs_str = ', '.join(sorted(valid_freqs))
|
|
499
498
|
raise ValueError(
|
|
500
499
|
f"Series '{s.series_id}' has frequency '{s.frequency}' which is faster than "
|
|
501
500
|
f"block '{block_name}' clock '{block_cfg.clock}'. "
|
|
502
501
|
f"Series in a block must have frequency <= block clock. "
|
|
503
|
-
f"
|
|
502
|
+
f"Suggested fix: change series frequency to one of [{valid_freqs_str}] "
|
|
503
|
+
f"(slower or equal to block clock '{block_cfg.clock}'), "
|
|
504
|
+
f"or set block clock to '{s.frequency}' or faster."
|
|
504
505
|
)
|
|
505
506
|
|
|
506
507
|
# Validate factors_per_block
|
|
@@ -538,6 +539,106 @@ class DFMConfig:
|
|
|
538
539
|
self._cached_blocks = np.array(blocks_list, dtype=int)
|
|
539
540
|
return self._cached_blocks
|
|
540
541
|
|
|
542
|
+
def validate_and_report(self) -> Dict[str, Any]:
|
|
543
|
+
"""Validate configuration and return structured report with issues and suggestions.
|
|
544
|
+
|
|
545
|
+
This method performs validation checks without raising exceptions, returning
|
|
546
|
+
a structured report that can be used for debugging and user guidance.
|
|
547
|
+
|
|
548
|
+
Returns
|
|
549
|
+
-------
|
|
550
|
+
Dict[str, Any]
|
|
551
|
+
Report dictionary with keys:
|
|
552
|
+
- 'valid': bool - Whether configuration is valid
|
|
553
|
+
- 'errors': List[str] - List of error messages
|
|
554
|
+
- 'warnings': List[str] - List of warning messages
|
|
555
|
+
- 'suggestions': List[str] - List of actionable suggestions
|
|
556
|
+
|
|
557
|
+
Examples
|
|
558
|
+
--------
|
|
559
|
+
>>> config = DFMConfig(...)
|
|
560
|
+
>>> report = config.validate_and_report()
|
|
561
|
+
>>> if not report['valid']:
|
|
562
|
+
... print("Errors:", report['errors'])
|
|
563
|
+
... print("Suggestions:", report['suggestions'])
|
|
564
|
+
"""
|
|
565
|
+
from .utils import FREQUENCY_HIERARCHY
|
|
566
|
+
|
|
567
|
+
report = {
|
|
568
|
+
'valid': True,
|
|
569
|
+
'errors': [],
|
|
570
|
+
'warnings': [],
|
|
571
|
+
'suggestions': []
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
# Check for empty series
|
|
575
|
+
if not self.series:
|
|
576
|
+
report['valid'] = False
|
|
577
|
+
report['errors'].append("DFM configuration must contain at least one series.")
|
|
578
|
+
report['suggestions'].append("Add series definitions to your configuration.")
|
|
579
|
+
return report
|
|
580
|
+
|
|
581
|
+
# Check for empty blocks
|
|
582
|
+
if not self.blocks:
|
|
583
|
+
report['valid'] = False
|
|
584
|
+
report['errors'].append("DFM configuration must contain at least one block.")
|
|
585
|
+
report['suggestions'].append("Add block definitions to your configuration.")
|
|
586
|
+
return report
|
|
587
|
+
|
|
588
|
+
# Check frequency constraints
|
|
589
|
+
global_clock_hierarchy = FREQUENCY_HIERARCHY.get(self.clock, 3)
|
|
590
|
+
for i, s in enumerate(self.series):
|
|
591
|
+
series_freq_hierarchy = FREQUENCY_HIERARCHY.get(s.frequency, 3)
|
|
592
|
+
|
|
593
|
+
for block_idx, loads_on_block in enumerate(s.blocks):
|
|
594
|
+
if loads_on_block == 1:
|
|
595
|
+
if block_idx < len(self.block_names):
|
|
596
|
+
block_name = self.block_names[block_idx]
|
|
597
|
+
block_cfg = self.blocks[block_name]
|
|
598
|
+
block_clock_hierarchy = FREQUENCY_HIERARCHY.get(block_cfg.clock, 3)
|
|
599
|
+
|
|
600
|
+
if series_freq_hierarchy < block_clock_hierarchy:
|
|
601
|
+
valid_freqs = [freq for freq, hier in FREQUENCY_HIERARCHY.items()
|
|
602
|
+
if hier >= block_clock_hierarchy]
|
|
603
|
+
valid_freqs_str = ', '.join(sorted(valid_freqs))
|
|
604
|
+
report['valid'] = False
|
|
605
|
+
report['errors'].append(
|
|
606
|
+
f"Series '{s.series_id}' has frequency '{s.frequency}' which is faster than "
|
|
607
|
+
f"block '{block_name}' clock '{block_cfg.clock}'."
|
|
608
|
+
)
|
|
609
|
+
report['suggestions'].append(
|
|
610
|
+
f"For series '{s.series_id}': change frequency to one of [{valid_freqs_str}], "
|
|
611
|
+
f"or set block '{block_name}' clock to '{s.frequency}' or faster."
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# Check block clock constraints
|
|
615
|
+
for block_name, block_cfg in self.blocks.items():
|
|
616
|
+
block_clock_hierarchy = FREQUENCY_HIERARCHY.get(block_cfg.clock, 3)
|
|
617
|
+
if block_clock_hierarchy < global_clock_hierarchy:
|
|
618
|
+
report['valid'] = False
|
|
619
|
+
report['errors'].append(
|
|
620
|
+
f"Block '{block_name}' has clock '{block_cfg.clock}' which is faster than "
|
|
621
|
+
f"global clock '{self.clock}'."
|
|
622
|
+
)
|
|
623
|
+
report['suggestions'].append(
|
|
624
|
+
f"Change block '{block_name}' clock to '{self.clock}' or slower, "
|
|
625
|
+
f"or set global clock to '{block_cfg.clock}' or faster."
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
# Check factors_per_block
|
|
629
|
+
if any(f < 1 for f in self.factors_per_block):
|
|
630
|
+
invalid_blocks = [i for i, f in enumerate(self.factors_per_block) if f < 1]
|
|
631
|
+
report['valid'] = False
|
|
632
|
+
report['errors'].append(
|
|
633
|
+
f"factors_per_block must contain positive integers (>= 1). "
|
|
634
|
+
f"Invalid values found at block indices {invalid_blocks}."
|
|
635
|
+
)
|
|
636
|
+
report['suggestions'].append(
|
|
637
|
+
f"Set factors_per_block[{invalid_blocks[0]}] to at least 1 for block '{self.block_names[invalid_blocks[0]]}'."
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
return report
|
|
641
|
+
|
|
541
642
|
# Note: Legacy PascalCase properties were removed to keep the API clean and generic.
|
|
542
643
|
# Use the snake_case helper methods above.
|
|
543
644
|
|
|
@@ -570,7 +671,12 @@ class DFMConfig:
|
|
|
570
671
|
'warn_on_regularization': data.get('warn_on_regularization', True),
|
|
571
672
|
'use_damped_updates': data.get('use_damped_updates', True),
|
|
572
673
|
'damping_factor': data.get('damping_factor', 0.8),
|
|
573
|
-
'warn_on_damped_update': data.get('warn_on_damped_update', True)
|
|
674
|
+
'warn_on_damped_update': data.get('warn_on_damped_update', True),
|
|
675
|
+
# Idiosyncratic component augmentation
|
|
676
|
+
'augment_idio': data.get('augment_idio', True),
|
|
677
|
+
'augment_idio_slow': data.get('augment_idio_slow', True),
|
|
678
|
+
'idio_rho0': data.get('idio_rho0', 0.1),
|
|
679
|
+
'idio_min_var': data.get('idio_min_var', 1e-8)
|
|
574
680
|
}
|
|
575
681
|
|
|
576
682
|
@classmethod
|
|
@@ -828,536 +934,104 @@ class DFMConfig:
|
|
|
828
934
|
return cls.from_dict(cfg)
|
|
829
935
|
|
|
830
936
|
|
|
831
|
-
# Register with Hydra ConfigStore (optional - only if Hydra is available)
|
|
832
|
-
if HYDRA_AVAILABLE and ConfigStore is not None:
|
|
833
|
-
try:
|
|
834
|
-
cs = ConfigStore.instance()
|
|
835
|
-
if cs is not None:
|
|
836
|
-
from dataclasses import dataclass as schema_dataclass
|
|
837
|
-
|
|
838
|
-
@schema_dataclass
|
|
839
|
-
class SeriesConfigSchema:
|
|
840
|
-
"""Schema for SeriesConfig validation in Hydra."""
|
|
841
|
-
series_id: str
|
|
842
|
-
series_name: str
|
|
843
|
-
frequency: str
|
|
844
|
-
units: str
|
|
845
|
-
transformation: str
|
|
846
|
-
category: str
|
|
847
|
-
blocks: List[int]
|
|
848
|
-
|
|
849
|
-
@schema_dataclass
|
|
850
|
-
class DFMConfigSchema:
|
|
851
|
-
"""Schema for unified DFMConfig validation in Hydra."""
|
|
852
|
-
series: List[SeriesConfigSchema]
|
|
853
|
-
block_names: List[str]
|
|
854
|
-
factors_per_block: Optional[List[int]] = None
|
|
855
|
-
ar_lag: int = 1
|
|
856
|
-
threshold: float = 1e-5
|
|
857
|
-
max_iter: int = 5000
|
|
858
|
-
nan_method: int = 2
|
|
859
|
-
nan_k: int = 3
|
|
860
|
-
clock: str = 'm'
|
|
861
|
-
|
|
862
|
-
# Register schemas
|
|
863
|
-
cs.store(group="dfm", name="base_dfm_config", node=DFMConfigSchema)
|
|
864
|
-
cs.store(group="model", name="base_model_config", node=DFMConfigSchema)
|
|
865
|
-
cs.store(name="dfm_config_schema", node=DFMConfigSchema)
|
|
866
|
-
cs.store(name="model_config_schema", node=DFMConfigSchema)
|
|
867
|
-
|
|
868
|
-
except Exception as e:
|
|
869
|
-
warnings.warn(f"Could not register Hydra structured config schemas: {e}. "
|
|
870
|
-
f"Configs will still work via from_dict() but without schema validation.")
|
|
871
|
-
|
|
872
|
-
|
|
873
937
|
# ============================================================================
|
|
874
938
|
# Configuration Source Adapters
|
|
875
939
|
# ============================================================================
|
|
876
940
|
|
|
877
|
-
|
|
941
|
+
# Import ConfigSource classes from separate module
|
|
942
|
+
from .config_sources import (
|
|
943
|
+
ConfigSource,
|
|
944
|
+
YamlSource,
|
|
945
|
+
DictSource,
|
|
946
|
+
SpecCSVSource,
|
|
947
|
+
HydraSource,
|
|
948
|
+
MergedConfigSource,
|
|
949
|
+
make_config_source,
|
|
950
|
+
)
|
|
878
951
|
|
|
952
|
+
# Re-export for backward compatibility
|
|
953
|
+
__all__ = [
|
|
954
|
+
'DFMConfig', 'SeriesConfig', 'BlockConfig', 'Params',
|
|
955
|
+
'DEFAULT_GLOBAL_BLOCK_NAME',
|
|
956
|
+
'ConfigSource', 'YamlSource', 'DictSource', 'SpecCSVSource',
|
|
957
|
+
'HydraSource', 'MergedConfigSource', 'make_config_source',
|
|
958
|
+
]
|
|
879
959
|
|
|
880
|
-
class
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
Any object that implements a `load()` method returning a DFMConfig
|
|
884
|
-
can be used as a configuration source.
|
|
885
|
-
"""
|
|
886
|
-
def load(self) -> DFMConfig:
|
|
887
|
-
"""Load and return a DFMConfig object."""
|
|
888
|
-
...
|
|
960
|
+
# Legacy class definitions removed - now in config_sources.py
|
|
961
|
+
# For backward compatibility, classes are imported and re-exported above
|
|
889
962
|
|
|
890
963
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
Supports Hydra-style configs with defaults for series and blocks.
|
|
895
|
-
"""
|
|
896
|
-
def __init__(self, yaml_path: Union[str, Path]):
|
|
897
|
-
"""Initialize YAML source.
|
|
898
|
-
|
|
899
|
-
Parameters
|
|
900
|
-
----------
|
|
901
|
-
yaml_path : str or Path
|
|
902
|
-
Path to YAML configuration file
|
|
903
|
-
"""
|
|
904
|
-
self.yaml_path = Path(yaml_path)
|
|
905
|
-
|
|
906
|
-
def load(self) -> DFMConfig:
|
|
907
|
-
"""Load configuration from YAML file."""
|
|
908
|
-
try:
|
|
909
|
-
from omegaconf import OmegaConf
|
|
910
|
-
except ImportError:
|
|
911
|
-
raise ImportError("omegaconf is required for YAML config loading. Install with: pip install omegaconf")
|
|
912
|
-
|
|
913
|
-
configfile = Path(self.yaml_path)
|
|
914
|
-
if not configfile.exists():
|
|
915
|
-
raise FileNotFoundError(f"Configuration file not found: {configfile}")
|
|
916
|
-
|
|
917
|
-
config_dir = configfile.parent
|
|
918
|
-
cfg = OmegaConf.load(configfile)
|
|
919
|
-
cfg_dict = OmegaConf.to_container(cfg, resolve=True)
|
|
920
|
-
|
|
921
|
-
# Extract main settings (estimation parameters)
|
|
922
|
-
excluded_keys = {'defaults', '_target_', '_recursive_', '_convert_'}
|
|
923
|
-
main_settings = {k: v for k, v in cfg_dict.items() if k not in excluded_keys}
|
|
924
|
-
|
|
925
|
-
# Load series from config/series/default.yaml
|
|
926
|
-
series_list = []
|
|
927
|
-
series_loaded = False
|
|
928
|
-
series_dict = None
|
|
929
|
-
|
|
930
|
-
if 'defaults' in cfg:
|
|
931
|
-
defaults_list = cfg.defaults
|
|
932
|
-
for default_item in defaults_list:
|
|
933
|
-
default_dict = OmegaConf.to_container(default_item, resolve=False) if hasattr(default_item, 'keys') else default_item
|
|
934
|
-
|
|
935
|
-
# Handle dict format: {'series': 'default'}
|
|
936
|
-
if isinstance(default_dict, dict) and 'series' in default_dict:
|
|
937
|
-
series_config_name = default_dict['series']
|
|
938
|
-
series_config_path = config_dir / 'series' / f'{series_config_name}.yaml'
|
|
939
|
-
if series_config_path.exists():
|
|
940
|
-
series_cfg = OmegaConf.load(series_config_path)
|
|
941
|
-
series_dict = OmegaConf.to_container(series_cfg, resolve=True)
|
|
942
|
-
series_loaded = True
|
|
943
|
-
break
|
|
944
|
-
|
|
945
|
-
# If not loaded from defaults, try direct path
|
|
946
|
-
if not series_loaded:
|
|
947
|
-
series_config_path = config_dir / 'series' / 'default.yaml'
|
|
948
|
-
if series_config_path.exists():
|
|
949
|
-
series_cfg = OmegaConf.load(series_config_path)
|
|
950
|
-
series_dict = OmegaConf.to_container(series_cfg, resolve=True)
|
|
951
|
-
series_loaded = True
|
|
952
|
-
|
|
953
|
-
# Convert series dict to SeriesConfig objects
|
|
954
|
-
if series_loaded and series_dict is not None:
|
|
955
|
-
for series_id, series_data in series_dict.items():
|
|
956
|
-
if isinstance(series_data, dict):
|
|
957
|
-
series_list.append(SeriesConfig(
|
|
958
|
-
series_id=series_id,
|
|
959
|
-
series_name=series_data.get('series_name', series_id),
|
|
960
|
-
frequency=series_data.get('frequency', 'm'),
|
|
961
|
-
transformation=series_data.get('transformation', 'lin'),
|
|
962
|
-
category=series_data.get('category', ''),
|
|
963
|
-
units=series_data.get('units', ''),
|
|
964
|
-
blocks=series_data.get('blocks', [])
|
|
965
|
-
))
|
|
966
|
-
|
|
967
|
-
# If no series loaded from separate files, try to get from main config
|
|
968
|
-
if not series_loaded and 'series' in cfg_dict:
|
|
969
|
-
series_data = cfg_dict['series']
|
|
970
|
-
if isinstance(series_data, list):
|
|
971
|
-
for series_item in series_data:
|
|
972
|
-
series_list.append(SeriesConfig(**series_item))
|
|
973
|
-
elif isinstance(series_data, dict):
|
|
974
|
-
for series_id, series_item in series_data.items():
|
|
975
|
-
if isinstance(series_item, dict):
|
|
976
|
-
series_item['series_id'] = series_id
|
|
977
|
-
series_list.append(SeriesConfig(**series_item))
|
|
978
|
-
|
|
979
|
-
# Load blocks from config/blocks/default.yaml
|
|
980
|
-
blocks_dict = {}
|
|
981
|
-
blocks_loaded = False
|
|
982
|
-
blocks_dict_raw = None
|
|
983
|
-
|
|
984
|
-
if 'defaults' in cfg:
|
|
985
|
-
defaults_list = cfg.defaults
|
|
986
|
-
for default_item in defaults_list:
|
|
987
|
-
default_dict = OmegaConf.to_container(default_item, resolve=False) if hasattr(default_item, 'keys') else default_item
|
|
988
|
-
|
|
989
|
-
# Handle dict format: {'blocks': 'default'}
|
|
990
|
-
if isinstance(default_dict, dict) and 'blocks' in default_dict:
|
|
991
|
-
blocks_config_name = default_dict['blocks']
|
|
992
|
-
blocks_config_path = config_dir / 'blocks' / f'{blocks_config_name}.yaml'
|
|
993
|
-
if blocks_config_path.exists():
|
|
994
|
-
blocks_cfg = OmegaConf.load(blocks_config_path)
|
|
995
|
-
blocks_dict_raw = OmegaConf.to_container(blocks_cfg, resolve=True)
|
|
996
|
-
blocks_loaded = True
|
|
997
|
-
break
|
|
998
|
-
|
|
999
|
-
# If not loaded from defaults, try direct path
|
|
1000
|
-
if not blocks_loaded:
|
|
1001
|
-
blocks_config_path = config_dir / 'blocks' / 'default.yaml'
|
|
1002
|
-
if blocks_config_path.exists():
|
|
1003
|
-
blocks_cfg = OmegaConf.load(blocks_config_path)
|
|
1004
|
-
blocks_dict_raw = OmegaConf.to_container(blocks_cfg, resolve=True)
|
|
1005
|
-
blocks_loaded = True
|
|
1006
|
-
|
|
1007
|
-
# Convert blocks dict to BlockConfig objects
|
|
1008
|
-
if blocks_loaded and blocks_dict_raw is not None:
|
|
1009
|
-
for block_name, block_data in blocks_dict_raw.items():
|
|
1010
|
-
if isinstance(block_data, dict):
|
|
1011
|
-
blocks_dict[block_name] = BlockConfig(
|
|
1012
|
-
factors=block_data.get('factors', 1),
|
|
1013
|
-
ar_lag=block_data.get('ar_lag', 1),
|
|
1014
|
-
clock=block_data.get('clock', 'm'),
|
|
1015
|
-
notes=block_data.get('notes', None)
|
|
1016
|
-
)
|
|
1017
|
-
|
|
1018
|
-
# If no blocks loaded from separate files, try to get from main config
|
|
1019
|
-
if not blocks_loaded and 'blocks' in cfg_dict:
|
|
1020
|
-
blocks_data = cfg_dict['blocks']
|
|
1021
|
-
if isinstance(blocks_data, dict):
|
|
1022
|
-
for block_name, block_item in blocks_data.items():
|
|
1023
|
-
if isinstance(block_item, dict):
|
|
1024
|
-
blocks_dict[block_name] = BlockConfig(**block_item)
|
|
1025
|
-
|
|
1026
|
-
# Ensure at least one block exists
|
|
1027
|
-
if not blocks_dict:
|
|
1028
|
-
blocks_dict[DEFAULT_GLOBAL_BLOCK_NAME] = BlockConfig(factors=1, clock=main_settings.get('clock', 'm'))
|
|
1029
|
-
|
|
1030
|
-
return DFMConfig(
|
|
1031
|
-
series=series_list,
|
|
1032
|
-
blocks=blocks_dict,
|
|
1033
|
-
**main_settings
|
|
1034
|
-
)
|
|
964
|
+
# ============================================================================
|
|
965
|
+
# Configuration validation functions
|
|
966
|
+
# ============================================================================
|
|
1035
967
|
|
|
968
|
+
# Valid frequency codes
|
|
969
|
+
_VALID_FREQUENCIES = {'d', 'w', 'm', 'q', 'sa', 'a'}
|
|
1036
970
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
- New format (list): {'series': [{'series_id': ..., ...}], ...}
|
|
1043
|
-
- Hydra format: {'series': {'series_id': {...}}, 'blocks': {...}}
|
|
1044
|
-
"""
|
|
1045
|
-
def __init__(self, mapping: Dict[str, Any]):
|
|
1046
|
-
"""Initialize dictionary source.
|
|
1047
|
-
|
|
1048
|
-
Parameters
|
|
1049
|
-
----------
|
|
1050
|
-
mapping : dict
|
|
1051
|
-
Dictionary containing configuration data
|
|
1052
|
-
"""
|
|
1053
|
-
self.mapping = mapping
|
|
1054
|
-
|
|
1055
|
-
def load(self) -> DFMConfig:
|
|
1056
|
-
"""Load configuration from dictionary.
|
|
1057
|
-
|
|
1058
|
-
If the dictionary is partial (e.g., only max_iter, threshold),
|
|
1059
|
-
it will be merged with a minimal default config.
|
|
1060
|
-
"""
|
|
1061
|
-
# Check if this is a partial config (missing series or blocks)
|
|
1062
|
-
has_series = 'series' in self.mapping and self.mapping['series']
|
|
1063
|
-
has_blocks = 'blocks' in self.mapping and self.mapping['blocks']
|
|
1064
|
-
|
|
1065
|
-
if not has_series or not has_blocks:
|
|
1066
|
-
# This is a partial config - create a minimal default and merge
|
|
1067
|
-
minimal_default = {
|
|
1068
|
-
'series': [],
|
|
1069
|
-
'blocks': {},
|
|
1070
|
-
'clock': 'm',
|
|
1071
|
-
'max_iter': 5000,
|
|
1072
|
-
'threshold': 1e-5
|
|
1073
|
-
}
|
|
1074
|
-
# Merge: mapping takes precedence
|
|
1075
|
-
merged = {**minimal_default, **self.mapping}
|
|
1076
|
-
return DFMConfig.from_dict(merged)
|
|
1077
|
-
|
|
1078
|
-
return DFMConfig.from_dict(self.mapping)
|
|
971
|
+
# Valid transformation codes
|
|
972
|
+
_VALID_TRANSFORMATIONS = {
|
|
973
|
+
'lin', 'chg', 'ch1', 'pch', 'pc1', 'pca',
|
|
974
|
+
'cch', 'cca', 'log'
|
|
975
|
+
}
|
|
1079
976
|
|
|
1080
977
|
|
|
1081
|
-
|
|
1082
|
-
"""
|
|
1083
|
-
|
|
1084
|
-
This source reads series definitions and block memberships from a CSV.
|
|
1085
|
-
It constructs a valid DFMConfig with sensible defaults for other
|
|
1086
|
-
parameters (e.g., clock='m', threshold=1e-5, max_iter=5000, factors=1).
|
|
1087
|
-
|
|
1088
|
-
For custom main settings (threshold, max_iter, clock, per-block factors),
|
|
1089
|
-
you can still merge with a base config via MergedConfigSource or
|
|
1090
|
-
dfm.load_config(base=..., override=spec_path).
|
|
1091
|
-
"""
|
|
1092
|
-
def __init__(self, spec_path: Union[str, Path]):
|
|
1093
|
-
"""Initialize spec CSV source.
|
|
1094
|
-
|
|
1095
|
-
Parameters
|
|
1096
|
-
----------
|
|
1097
|
-
spec_path : str or Path
|
|
1098
|
-
Path to spec CSV file
|
|
1099
|
-
"""
|
|
1100
|
-
self.spec_path = Path(spec_path)
|
|
978
|
+
def validate_frequency(frequency: str) -> str:
|
|
979
|
+
"""Validate frequency code.
|
|
1101
980
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
specfile = Path(self.spec_path)
|
|
1108
|
-
if not specfile.exists():
|
|
1109
|
-
raise FileNotFoundError(f"Spec file not found: {specfile}")
|
|
981
|
+
Parameters
|
|
982
|
+
----------
|
|
983
|
+
frequency : str
|
|
984
|
+
Frequency code to validate
|
|
1110
985
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
986
|
+
Returns
|
|
987
|
+
-------
|
|
988
|
+
str
|
|
989
|
+
Validated frequency code (same as input if valid)
|
|
1115
990
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
"""Load configuration from a Hydra DictConfig or dict.
|
|
1121
|
-
|
|
1122
|
-
This adapter handles Hydra's composed configuration objects,
|
|
1123
|
-
converting them to DFMConfig format.
|
|
1124
|
-
"""
|
|
1125
|
-
def __init__(self, cfg: Union[Dict[str, Any], 'DictConfig']):
|
|
1126
|
-
"""Initialize Hydra source.
|
|
991
|
+
Raises
|
|
992
|
+
------
|
|
993
|
+
ValueError
|
|
994
|
+
If frequency is not in the set of valid frequencies
|
|
1127
995
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
def load(self) -> DFMConfig:
|
|
1136
|
-
"""Load configuration from Hydra DictConfig/dict."""
|
|
1137
|
-
return DFMConfig.from_hydra(self.cfg)
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
class MergedConfigSource:
|
|
1141
|
-
"""Merge multiple configuration sources.
|
|
1142
|
-
|
|
1143
|
-
This allows combining configurations from different sources,
|
|
1144
|
-
e.g., base YAML config + series from Spec CSV.
|
|
1145
|
-
|
|
1146
|
-
The merge strategy:
|
|
1147
|
-
- Base config provides main settings (threshold, max_iter, clock, blocks)
|
|
1148
|
-
- Override config provides series definitions (replaces base series)
|
|
1149
|
-
- Block definitions are merged (override takes precedence)
|
|
996
|
+
Examples
|
|
997
|
+
--------
|
|
998
|
+
>>> validate_frequency('m')
|
|
999
|
+
'm'
|
|
1000
|
+
>>> validate_frequency('invalid')
|
|
1001
|
+
ValueError: Invalid frequency: invalid. Must be one of {'d', 'w', 'm', 'q', 'sa', 'a'}
|
|
1150
1002
|
"""
|
|
1151
|
-
|
|
1152
|
-
"
|
|
1153
|
-
|
|
1154
|
-
Parameters
|
|
1155
|
-
----------
|
|
1156
|
-
base : ConfigSource
|
|
1157
|
-
Base configuration (provides main settings)
|
|
1158
|
-
override : ConfigSource
|
|
1159
|
-
Override configuration (provides series/block overrides)
|
|
1160
|
-
"""
|
|
1161
|
-
self.base = base
|
|
1162
|
-
self.override = override
|
|
1163
|
-
|
|
1164
|
-
def load(self) -> DFMConfig:
|
|
1165
|
-
"""Load and merge configurations."""
|
|
1166
|
-
from dataclasses import fields
|
|
1167
|
-
|
|
1168
|
-
base_cfg = self.base.load()
|
|
1169
|
-
|
|
1170
|
-
# Check if override is a partial config (DictSource with partial dict)
|
|
1171
|
-
override_is_partial = False
|
|
1172
|
-
if isinstance(self.override, DictSource):
|
|
1173
|
-
has_series = 'series' in self.override.mapping and self.override.mapping['series']
|
|
1174
|
-
has_blocks = 'blocks' in self.override.mapping and self.override.mapping['blocks']
|
|
1175
|
-
override_is_partial = not (has_series and has_blocks)
|
|
1176
|
-
|
|
1177
|
-
if override_is_partial:
|
|
1178
|
-
# Handle partial override: merge fields directly without loading full DFMConfig
|
|
1179
|
-
override_dict = self.override.mapping
|
|
1180
|
-
override_cfg = base_cfg # Use base as template
|
|
1181
|
-
else:
|
|
1182
|
-
override_cfg = self.override.load()
|
|
1183
|
-
|
|
1184
|
-
# Merge blocks: override takes precedence
|
|
1185
|
-
if override_is_partial and 'blocks' in override_dict:
|
|
1186
|
-
# Merge block dicts
|
|
1187
|
-
merged_blocks = {**base_cfg.blocks, **override_dict['blocks']}
|
|
1188
|
-
else:
|
|
1189
|
-
merged_blocks = {**base_cfg.blocks, **override_cfg.blocks}
|
|
1190
|
-
|
|
1191
|
-
# Use override's series if provided and non-empty, otherwise use base's series
|
|
1192
|
-
if override_is_partial:
|
|
1193
|
-
if 'series' in override_dict and override_dict['series']:
|
|
1194
|
-
merged_series = override_dict['series']
|
|
1195
|
-
else:
|
|
1196
|
-
merged_series = base_cfg.series
|
|
1197
|
-
else:
|
|
1198
|
-
merged_series = override_cfg.series if (override_cfg.series and len(override_cfg.series) > 0) else base_cfg.series
|
|
1199
|
-
|
|
1200
|
-
# Get all config fields (excluding derived/computed fields)
|
|
1201
|
-
excluded_fields = {'series', 'blocks', 'block_names', 'factors_per_block', '_cached_blocks'}
|
|
1202
|
-
base_settings = {
|
|
1203
|
-
field.name: getattr(base_cfg, field.name)
|
|
1204
|
-
for field in fields(DFMConfig)
|
|
1205
|
-
if field.name not in excluded_fields
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
# Override settings from override_cfg or override_dict
|
|
1209
|
-
if override_is_partial:
|
|
1210
|
-
override_settings = {
|
|
1211
|
-
field.name: override_dict.get(field.name, getattr(base_cfg, field.name))
|
|
1212
|
-
for field in fields(DFMConfig)
|
|
1213
|
-
if field.name not in excluded_fields
|
|
1214
|
-
}
|
|
1215
|
-
else:
|
|
1216
|
-
override_settings = {
|
|
1217
|
-
field.name: getattr(override_cfg, field.name)
|
|
1218
|
-
for field in fields(DFMConfig)
|
|
1219
|
-
if field.name not in excluded_fields
|
|
1220
|
-
and hasattr(override_cfg, field.name)
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
# Merge settings: base + override (override takes precedence)
|
|
1224
|
-
merged_settings = {**base_settings, **override_settings}
|
|
1225
|
-
|
|
1226
|
-
# Create merged config: merged settings + merged series + merged blocks
|
|
1227
|
-
return DFMConfig(
|
|
1228
|
-
series=merged_series,
|
|
1229
|
-
blocks=merged_blocks,
|
|
1230
|
-
**merged_settings
|
|
1231
|
-
)
|
|
1003
|
+
if frequency not in _VALID_FREQUENCIES:
|
|
1004
|
+
raise ValueError(f"Invalid frequency: {frequency}. Must be one of {_VALID_FREQUENCIES}")
|
|
1005
|
+
return frequency
|
|
1232
1006
|
|
|
1233
1007
|
|
|
1234
|
-
def
|
|
1235
|
-
|
|
1236
|
-
*,
|
|
1237
|
-
yaml: Optional[Union[str, Path]] = None,
|
|
1238
|
-
mapping: Optional[Union[Dict[str, Any], Any]] = None,
|
|
1239
|
-
spec: Optional[Union[str, Path]] = None,
|
|
1240
|
-
hydra: Optional[Union[Dict[str, Any], 'DictConfig']] = None,
|
|
1241
|
-
) -> ConfigSource:
|
|
1242
|
-
"""Create a ConfigSource adapter from various input formats.
|
|
1243
|
-
|
|
1244
|
-
This factory function automatically selects the appropriate adapter
|
|
1245
|
-
based on the input type or explicit keyword arguments.
|
|
1008
|
+
def validate_transformation(transformation: str) -> str:
|
|
1009
|
+
"""Validate transformation code.
|
|
1246
1010
|
|
|
1247
1011
|
Parameters
|
|
1248
1012
|
----------
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
If str/Path, treated as YAML file path.
|
|
1252
|
-
If dict, treated as dictionary config.
|
|
1253
|
-
yaml : str or Path, optional
|
|
1254
|
-
Explicit YAML file path
|
|
1255
|
-
mapping : dict, optional
|
|
1256
|
-
Explicit dictionary config
|
|
1257
|
-
spec : str or Path, optional
|
|
1258
|
-
Explicit spec CSV file path
|
|
1259
|
-
hydra : DictConfig or dict, optional
|
|
1260
|
-
Explicit Hydra config
|
|
1013
|
+
transformation : str
|
|
1014
|
+
Transformation code to validate
|
|
1261
1015
|
|
|
1262
1016
|
Returns
|
|
1263
1017
|
-------
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1018
|
+
str
|
|
1019
|
+
Validated transformation code (same as input, even if unknown)
|
|
1020
|
+
|
|
1021
|
+
Notes
|
|
1022
|
+
-----
|
|
1023
|
+
Unknown transformation codes trigger a warning but are not rejected,
|
|
1024
|
+
allowing for extensibility. The code will be used as-is, and the
|
|
1025
|
+
transformation logic should handle unknown codes appropriately.
|
|
1026
|
+
|
|
1267
1027
|
Examples
|
|
1268
1028
|
--------
|
|
1269
|
-
>>>
|
|
1270
|
-
|
|
1271
|
-
>>>
|
|
1272
|
-
|
|
1273
|
-
>>> source = make_config_source({'series': [...], 'clock': 'm'})
|
|
1274
|
-
>>>
|
|
1275
|
-
>>> # Explicit keyword arguments
|
|
1276
|
-
>>> source = make_config_source(yaml='config/default.yaml')
|
|
1277
|
-
>>> source = make_config_source(spec='data/spec.csv')
|
|
1278
|
-
>>>
|
|
1279
|
-
>>> # Merge YAML base + Spec CSV series
|
|
1280
|
-
>>> base = make_config_source(yaml='config/default.yaml')
|
|
1281
|
-
>>> override = make_config_source(spec='data/spec.csv')
|
|
1282
|
-
>>> merged = MergedConfigSource(base, override)
|
|
1029
|
+
>>> validate_transformation('lin')
|
|
1030
|
+
'lin'
|
|
1031
|
+
>>> validate_transformation('unknown') # Issues warning but returns value
|
|
1032
|
+
'unknown'
|
|
1283
1033
|
"""
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
f"Only one of yaml, mapping, spec, or hydra can be specified. "
|
|
1289
|
-
f"Got: {', '.join(explicit_kwargs)}. "
|
|
1290
|
-
f"For merging configs, use MergedConfigSource."
|
|
1291
|
-
)
|
|
1292
|
-
|
|
1293
|
-
# Helper: coerce arbitrary object with attributes into dict
|
|
1294
|
-
def _coerce_to_mapping(obj: Any) -> Dict[str, Any]:
|
|
1295
|
-
if isinstance(obj, dict):
|
|
1296
|
-
return obj
|
|
1297
|
-
if is_dataclass(obj):
|
|
1298
|
-
return asdict(obj)
|
|
1299
|
-
if hasattr(obj, "__dict__"):
|
|
1300
|
-
try:
|
|
1301
|
-
return dict(vars(obj))
|
|
1302
|
-
except Exception:
|
|
1303
|
-
pass
|
|
1304
|
-
raise TypeError(
|
|
1305
|
-
f"Unsupported mapping type: {type(obj)}. "
|
|
1306
|
-
f"Provide a dict, dataclass instance, or an object with attributes."
|
|
1307
|
-
)
|
|
1308
|
-
|
|
1309
|
-
# Handle explicit keyword arguments
|
|
1310
|
-
if yaml is not None:
|
|
1311
|
-
return YamlSource(yaml)
|
|
1312
|
-
if mapping is not None:
|
|
1313
|
-
return DictSource(_coerce_to_mapping(mapping))
|
|
1314
|
-
if spec is not None:
|
|
1315
|
-
return SpecCSVSource(spec)
|
|
1316
|
-
if hydra is not None:
|
|
1317
|
-
return HydraSource(hydra)
|
|
1318
|
-
|
|
1319
|
-
# Infer from source argument
|
|
1320
|
-
if source is None:
|
|
1321
|
-
raise ValueError(
|
|
1322
|
-
"No configuration source provided. "
|
|
1323
|
-
"Specify source, yaml, mapping, spec, or hydra."
|
|
1324
|
-
)
|
|
1325
|
-
|
|
1326
|
-
# If already a ConfigSource, return as-is
|
|
1327
|
-
if hasattr(source, 'load') and callable(getattr(source, 'load')):
|
|
1328
|
-
return source # type: ignore
|
|
1329
|
-
|
|
1330
|
-
# Infer type from source
|
|
1331
|
-
if isinstance(source, DFMConfig):
|
|
1332
|
-
# Wrap DFMConfig in a simple adapter
|
|
1333
|
-
class DFMConfigAdapter:
|
|
1334
|
-
def __init__(self, cfg: DFMConfig):
|
|
1335
|
-
self._cfg = cfg
|
|
1336
|
-
def load(self) -> DFMConfig:
|
|
1337
|
-
return self._cfg
|
|
1338
|
-
return DFMConfigAdapter(source)
|
|
1339
|
-
|
|
1340
|
-
if isinstance(source, (str, Path)):
|
|
1341
|
-
path = Path(source)
|
|
1342
|
-
suffix = path.suffix.lower()
|
|
1343
|
-
if suffix in ['.yaml', '.yml']:
|
|
1344
|
-
return YamlSource(path)
|
|
1345
|
-
elif suffix == '.csv':
|
|
1346
|
-
return SpecCSVSource(path)
|
|
1347
|
-
else:
|
|
1348
|
-
# Default to YAML if extension unclear
|
|
1349
|
-
return YamlSource(path)
|
|
1350
|
-
|
|
1351
|
-
if isinstance(source, dict):
|
|
1352
|
-
return DictSource(source)
|
|
1353
|
-
# Accept objects that can be coerced into dict (dataclass or attribute bag)
|
|
1354
|
-
try:
|
|
1355
|
-
coerced = _coerce_to_mapping(source)
|
|
1356
|
-
return DictSource(coerced)
|
|
1357
|
-
except Exception:
|
|
1358
|
-
pass
|
|
1359
|
-
|
|
1360
|
-
raise TypeError(
|
|
1361
|
-
f"Unsupported source type: {type(source)}. "
|
|
1362
|
-
f"Expected str, Path, dict, ConfigSource, or DFMConfig."
|
|
1363
|
-
)
|
|
1034
|
+
import warnings
|
|
1035
|
+
if transformation not in _VALID_TRANSFORMATIONS:
|
|
1036
|
+
warnings.warn(f"Unknown transformation code: {transformation}. Will use untransformed data.")
|
|
1037
|
+
return transformation
|