timber-common 0.2.9__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.
- common/__init__.py +329 -0
- common/config/__init__.py +16 -0
- common/config/model_loader.py +258 -0
- common/engine/__init__.py +0 -0
- common/engine/config_executor.py +317 -0
- common/engine/operation_registry.py +173 -0
- common/init.py +318 -0
- common/models/__init__.py +22 -0
- common/models/base.py +399 -0
- common/models/configs/__init__.py +0 -0
- common/models/core/__init.__.py +14 -0
- common/models/core/tag.py +53 -0
- common/models/core/user.py +299 -0
- common/models/factory.py +654 -0
- common/models/mixins.py +327 -0
- common/models/registry.py +267 -0
- common/services/__init__.py +41 -0
- common/services/data_fetcher/__init__.py +49 -0
- common/services/data_fetcher/alphavantage.py +190 -0
- common/services/data_fetcher/base.py +127 -0
- common/services/data_fetcher/curated_data.py +197 -0
- common/services/data_fetcher/polygon.py +244 -0
- common/services/data_fetcher/stock.py +268 -0
- common/services/data_fetcher/yfinance.py +154 -0
- common/services/data_processor/__init__.py +232 -0
- common/services/data_processor/portfolio_metrics.py +261 -0
- common/services/data_processor/returns.py +112 -0
- common/services/data_processor/risk_metrics.py +231 -0
- common/services/data_processor/standardization.py +129 -0
- common/services/data_processor/technical_indicators.py +281 -0
- common/services/db_service.py +268 -0
- common/services/encryption/__init__.py +0 -0
- common/services/encryption/field_encryption.py +349 -0
- common/services/gdpr/__init__.py +13 -0
- common/services/gdpr/deletion.py +433 -0
- common/services/inventory/__init__.py +74 -0
- common/services/inventory/available_capabilities.py +602 -0
- common/services/inventory/cached_capabilities.py +557 -0
- common/services/inventory/loader.py +331 -0
- common/services/persistence/__init__.py +94 -0
- common/services/persistence/base.py +300 -0
- common/services/persistence/cache.py +405 -0
- common/services/persistence/instances.py +109 -0
- common/services/persistence/manager.py +218 -0
- common/services/persistence/notification.py +400 -0
- common/services/persistence/research.py +331 -0
- common/services/persistence/session.py +690 -0
- common/services/persistence/tracker.py +434 -0
- common/services/security/__init__.py +0 -0
- common/services/security/oauth_service.py +806 -0
- common/services/vector/__init__.py +23 -0
- common/services/vector/auto_ingestion.py +452 -0
- common/services/vector/tag_embedding.py +394 -0
- common/utils/__init__.py +57 -0
- common/utils/config.py +296 -0
- common/utils/db_utils.py +119 -0
- common/utils/helpers.py +151 -0
- common/utils/time_helpers.py +6 -0
- common/utils/validators.py +211 -0
- modules/__init__.py +0 -0
- modules/config/custom_analysis.yaml +30 -0
- modules/config/investing_operations_config.yaml +182 -0
- modules/investing_operations.py +431 -0
- timber_common-0.2.9.dist-info/METADATA +651 -0
- timber_common-0.2.9.dist-info/RECORD +67 -0
- timber_common-0.2.9.dist-info/WHEEL +4 -0
- timber_common-0.2.9.dist-info/licenses/LICENSE +59 -0
common/__init__.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# timber/common/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Timber Common - Shared library for OakQuant workflow system
|
|
4
|
+
|
|
5
|
+
A comprehensive library providing:
|
|
6
|
+
- Config-driven model creation (NEW)
|
|
7
|
+
- Modular persistence services (NEW)
|
|
8
|
+
- Field-level encryption (NEW)
|
|
9
|
+
- GDPR compliance (NEW)
|
|
10
|
+
- Vector search integration (NEW)
|
|
11
|
+
- Data fetching from multiple financial APIs (yfinance, Alpha Vantage, Polygon.io)
|
|
12
|
+
- Database utilities and ORM support via SQLAlchemy
|
|
13
|
+
- Data validation and helpers
|
|
14
|
+
- Curated company data management
|
|
15
|
+
|
|
16
|
+
Quick Start (New Architecture):
|
|
17
|
+
from timber.common import initialize_timber, get_model
|
|
18
|
+
|
|
19
|
+
# Initialize Timber
|
|
20
|
+
initialize_timber(
|
|
21
|
+
model_config_dirs=['./config/models'],
|
|
22
|
+
enable_encryption=True
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Use dynamic models
|
|
26
|
+
User = get_model('User')
|
|
27
|
+
StockResearchSession = get_model('StockResearchSession')
|
|
28
|
+
|
|
29
|
+
Quick Start (Legacy Services):
|
|
30
|
+
from timber.common.services import stock_data_service
|
|
31
|
+
|
|
32
|
+
# Fetch stock data
|
|
33
|
+
df, error = stock_data_service.fetch_historical_data('AAPL', period='1y')
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
__version__ = "0.2.0"
|
|
37
|
+
|
|
38
|
+
# =============================================================================
|
|
39
|
+
# NEW ARCHITECTURE - Core Timber Components
|
|
40
|
+
# =============================================================================
|
|
41
|
+
|
|
42
|
+
from .init import (
|
|
43
|
+
initialize_timber,
|
|
44
|
+
shutdown_timber,
|
|
45
|
+
get_initialization_status,
|
|
46
|
+
is_initialized,
|
|
47
|
+
reset_initialization
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Import config instance and class
|
|
51
|
+
from .utils.config import config, Config
|
|
52
|
+
from .config.model_loader import model_loader, ModelConfigLoader
|
|
53
|
+
|
|
54
|
+
from .models.base import Base, db_manager
|
|
55
|
+
from .models.registry import (
|
|
56
|
+
model_registry,
|
|
57
|
+
register_model,
|
|
58
|
+
get_model,
|
|
59
|
+
get_session_model
|
|
60
|
+
)
|
|
61
|
+
from .models.factory import model_factory
|
|
62
|
+
|
|
63
|
+
from .models.mixins import (
|
|
64
|
+
TimestampMixin,
|
|
65
|
+
SoftDeleteMixin,
|
|
66
|
+
EncryptedFieldMixin,
|
|
67
|
+
GDPRComplianceMixin,
|
|
68
|
+
SearchableMixin,
|
|
69
|
+
CacheableMixin,
|
|
70
|
+
AuditMixin
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# CORE SERVICES
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
# These are your existing services from the old architecture
|
|
78
|
+
# Import them conditionally to avoid breaking existing code
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
from common.services.data_fetcher import stock_data_service, StockDataService
|
|
82
|
+
STOCK_DATA_AVAILABLE = True
|
|
83
|
+
except ImportError:
|
|
84
|
+
STOCK_DATA_AVAILABLE = False
|
|
85
|
+
stock_data_service = None
|
|
86
|
+
StockDataService = None
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
from common.services.data_fetcher import curated_data_loader, CuratedDataLoader
|
|
90
|
+
CURATED_DATA_AVAILABLE = True
|
|
91
|
+
except ImportError:
|
|
92
|
+
CURATED_DATA_AVAILABLE = False
|
|
93
|
+
curated_data_loader = None
|
|
94
|
+
CuratedDataLoader = None
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
from .services.db_service import db_service, DBService, get_db
|
|
98
|
+
DB_SERVICE_AVAILABLE = True
|
|
99
|
+
except ImportError:
|
|
100
|
+
# Fall back to new db_manager
|
|
101
|
+
DB_SERVICE_AVAILABLE = False
|
|
102
|
+
db_service = None
|
|
103
|
+
DBService = None
|
|
104
|
+
# Use new architecture's get_db
|
|
105
|
+
get_db = db_manager.get_db_session
|
|
106
|
+
|
|
107
|
+
# =============================================================================
|
|
108
|
+
# UTILITIES
|
|
109
|
+
# =============================================================================
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
from .utils.helpers import (
|
|
113
|
+
parse_natural_period_to_dates,
|
|
114
|
+
standardize_symbol,
|
|
115
|
+
format_currency,
|
|
116
|
+
)
|
|
117
|
+
HELPERS_AVAILABLE = True
|
|
118
|
+
except ImportError:
|
|
119
|
+
HELPERS_AVAILABLE = False
|
|
120
|
+
parse_natural_period_to_dates = None
|
|
121
|
+
standardize_symbol = None
|
|
122
|
+
format_currency = None
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
from .utils.validators import (
|
|
126
|
+
validate_stock_symbol,
|
|
127
|
+
validate_date_string,
|
|
128
|
+
validate_date_range,
|
|
129
|
+
validate_dataframe,
|
|
130
|
+
validate_price_data,
|
|
131
|
+
)
|
|
132
|
+
VALIDATORS_AVAILABLE = True
|
|
133
|
+
except ImportError:
|
|
134
|
+
VALIDATORS_AVAILABLE = False
|
|
135
|
+
validate_stock_symbol = None
|
|
136
|
+
validate_date_string = None
|
|
137
|
+
validate_date_range = None
|
|
138
|
+
validate_dataframe = None
|
|
139
|
+
validate_price_data = None
|
|
140
|
+
|
|
141
|
+
# =============================================================================
|
|
142
|
+
# SUBMODULES
|
|
143
|
+
# =============================================================================
|
|
144
|
+
|
|
145
|
+
from . import models
|
|
146
|
+
|
|
147
|
+
# =============================================================================
|
|
148
|
+
# PUBLIC API
|
|
149
|
+
# =============================================================================
|
|
150
|
+
|
|
151
|
+
__all__ = [
|
|
152
|
+
# Version
|
|
153
|
+
'__version__',
|
|
154
|
+
|
|
155
|
+
# ===== NEW ARCHITECTURE =====
|
|
156
|
+
|
|
157
|
+
# Initialization
|
|
158
|
+
'initialize_timber',
|
|
159
|
+
'shutdown_timber',
|
|
160
|
+
'get_initialization_status',
|
|
161
|
+
'is_initialized',
|
|
162
|
+
'reset_initialization',
|
|
163
|
+
|
|
164
|
+
# Configuration
|
|
165
|
+
'config',
|
|
166
|
+
'Config',
|
|
167
|
+
'model_loader',
|
|
168
|
+
'ModelConfigLoader',
|
|
169
|
+
|
|
170
|
+
# Database
|
|
171
|
+
'Base',
|
|
172
|
+
'db_manager',
|
|
173
|
+
'get_db',
|
|
174
|
+
|
|
175
|
+
# Models
|
|
176
|
+
'model_registry',
|
|
177
|
+
'register_model',
|
|
178
|
+
'get_model',
|
|
179
|
+
'get_session_model',
|
|
180
|
+
'model_factory',
|
|
181
|
+
|
|
182
|
+
# Mixins
|
|
183
|
+
'TimestampMixin',
|
|
184
|
+
'SoftDeleteMixin',
|
|
185
|
+
'EncryptedFieldMixin',
|
|
186
|
+
'GDPRComplianceMixin',
|
|
187
|
+
'SearchableMixin',
|
|
188
|
+
'CacheableMixin',
|
|
189
|
+
'AuditMixin',
|
|
190
|
+
|
|
191
|
+
# ===== CORE SERVICES =====
|
|
192
|
+
|
|
193
|
+
# Stock data service
|
|
194
|
+
'stock_data_service',
|
|
195
|
+
'StockDataService',
|
|
196
|
+
|
|
197
|
+
# Curated data service
|
|
198
|
+
'curated_data_loader',
|
|
199
|
+
'CuratedDataLoader',
|
|
200
|
+
|
|
201
|
+
# DB service (legacy)
|
|
202
|
+
'db_service',
|
|
203
|
+
'DBService',
|
|
204
|
+
|
|
205
|
+
# ===== UTILITIES =====
|
|
206
|
+
|
|
207
|
+
# Helpers
|
|
208
|
+
'parse_natural_period_to_dates',
|
|
209
|
+
'standardize_symbol',
|
|
210
|
+
'format_currency',
|
|
211
|
+
|
|
212
|
+
# Validators
|
|
213
|
+
'validate_stock_symbol',
|
|
214
|
+
'validate_date_string',
|
|
215
|
+
'validate_date_range',
|
|
216
|
+
'validate_dataframe',
|
|
217
|
+
'validate_price_data',
|
|
218
|
+
|
|
219
|
+
# ===== SUBMODULES =====
|
|
220
|
+
|
|
221
|
+
'models',
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# =============================================================================
|
|
226
|
+
# CONVENIENCE FUNCTIONS
|
|
227
|
+
# =============================================================================
|
|
228
|
+
|
|
229
|
+
def get_version() -> str:
|
|
230
|
+
"""Return the current version of timber_common."""
|
|
231
|
+
return __version__
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def get_available_features() -> dict:
|
|
235
|
+
"""
|
|
236
|
+
Get information about available features.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Dictionary showing which features are available
|
|
240
|
+
"""
|
|
241
|
+
return {
|
|
242
|
+
'version': __version__,
|
|
243
|
+
'new_architecture': {
|
|
244
|
+
'models': True,
|
|
245
|
+
'persistence': True,
|
|
246
|
+
'encryption': True,
|
|
247
|
+
'gdpr': True,
|
|
248
|
+
'vector_search': True,
|
|
249
|
+
},
|
|
250
|
+
'legacy_services': {
|
|
251
|
+
'stock_data_service': STOCK_DATA_AVAILABLE,
|
|
252
|
+
'curated_data_loader': CURATED_DATA_AVAILABLE,
|
|
253
|
+
'db_service': DB_SERVICE_AVAILABLE,
|
|
254
|
+
},
|
|
255
|
+
'utilities': {
|
|
256
|
+
'helpers': HELPERS_AVAILABLE,
|
|
257
|
+
'validators': VALIDATORS_AVAILABLE,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def validate_configuration() -> dict:
|
|
263
|
+
"""
|
|
264
|
+
Validate the current configuration.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Dictionary with validation results
|
|
268
|
+
"""
|
|
269
|
+
results = {
|
|
270
|
+
'new_architecture': True,
|
|
271
|
+
'database_url': config.DATABASE_URL is not None,
|
|
272
|
+
'encryption_key': config.ENCRYPTION_KEY is not None,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# Test database connection if initialized
|
|
276
|
+
if db_manager._engine:
|
|
277
|
+
results['database_connected'] = db_manager.check_connection()
|
|
278
|
+
else:
|
|
279
|
+
results['database_connected'] = None
|
|
280
|
+
results['note'] = 'Database not initialized. Call initialize_timber() first.'
|
|
281
|
+
|
|
282
|
+
# Check legacy services
|
|
283
|
+
if DB_SERVICE_AVAILABLE and db_service:
|
|
284
|
+
try:
|
|
285
|
+
results['legacy_db_healthy'] = db_service.health_check()
|
|
286
|
+
except Exception as e:
|
|
287
|
+
results['legacy_db_healthy'] = False
|
|
288
|
+
results['legacy_db_error'] = str(e)
|
|
289
|
+
|
|
290
|
+
return results
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def print_status():
|
|
294
|
+
"""Print current Timber status to console."""
|
|
295
|
+
print("=" * 60)
|
|
296
|
+
print("Timber Common Library Status")
|
|
297
|
+
print("=" * 60)
|
|
298
|
+
|
|
299
|
+
features = get_available_features()
|
|
300
|
+
|
|
301
|
+
print(f"\nVersion: {features['version']}")
|
|
302
|
+
|
|
303
|
+
print("\n🆕 New Architecture:")
|
|
304
|
+
for feature, available in features['new_architecture'].items():
|
|
305
|
+
status = "✓" if available else "✗"
|
|
306
|
+
print(f" {status} {feature}")
|
|
307
|
+
|
|
308
|
+
print("\n📦 Legacy Services:")
|
|
309
|
+
for service, available in features['legacy_services'].items():
|
|
310
|
+
status = "✓" if available else "✗"
|
|
311
|
+
print(f" {status} {service}")
|
|
312
|
+
|
|
313
|
+
print("\n🔧 Utilities:")
|
|
314
|
+
for util, available in features['utilities'].items():
|
|
315
|
+
status = "✓" if available else "✗"
|
|
316
|
+
print(f" {status} {util}")
|
|
317
|
+
|
|
318
|
+
if db_manager._engine:
|
|
319
|
+
status = get_initialization_status()
|
|
320
|
+
print(f"\n📊 Database:")
|
|
321
|
+
print(f" Models registered: {status['models_registered']}")
|
|
322
|
+
print(f" Session types: {status['session_types']}")
|
|
323
|
+
print(f" Tables: {status['database_tables']}")
|
|
324
|
+
print(f" Connected: {status['database_connected']}")
|
|
325
|
+
else:
|
|
326
|
+
print(f"\n📊 Database: Not initialized")
|
|
327
|
+
print(f" Call initialize_timber() to set up")
|
|
328
|
+
|
|
329
|
+
print("\n" + "=" * 60)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# timber/common/config/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Configuration Module
|
|
4
|
+
|
|
5
|
+
Provides configuration management and model loading capabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from common.utils.config import config, Config
|
|
9
|
+
from .model_loader import model_loader, ModelConfigLoader
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'config',
|
|
13
|
+
'Config',
|
|
14
|
+
'model_loader',
|
|
15
|
+
'ModelConfigLoader',
|
|
16
|
+
]
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# timber/common/config/model_loader.py
|
|
2
|
+
"""
|
|
3
|
+
Model Configuration Loader - ENHANCED VERSION
|
|
4
|
+
|
|
5
|
+
Loads model definitions from YAML configuration files with dependency resolution.
|
|
6
|
+
Supports 'depends' attribute to control loading order.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Optional, Dict, Set
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
from ..models.factory import model_factory
|
|
15
|
+
from ..models.registry import model_registry
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ModelConfigLoader:
|
|
21
|
+
"""
|
|
22
|
+
Loads and creates models from YAML configuration files.
|
|
23
|
+
|
|
24
|
+
Supports dependency-based loading order via 'depends' attribute in YAML files.
|
|
25
|
+
|
|
26
|
+
Example YAML with dependencies:
|
|
27
|
+
version: "1.0.0"
|
|
28
|
+
depends: ["00_association_tables.yaml"] # Load this first
|
|
29
|
+
models:
|
|
30
|
+
- name: MyModel
|
|
31
|
+
...
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.loaded_configs: List[str] = []
|
|
36
|
+
self.loaded_models: List[str] = []
|
|
37
|
+
|
|
38
|
+
def load_from_file(self, config_path: str) -> List:
|
|
39
|
+
"""
|
|
40
|
+
Load models from a single YAML config file.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
config_path: Path to YAML configuration file
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
List of created model classes
|
|
47
|
+
"""
|
|
48
|
+
logger.info(f"Loading models from: {config_path}")
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
models = model_factory.create_model_from_config_file(config_path)
|
|
52
|
+
|
|
53
|
+
self.loaded_configs.append(config_path)
|
|
54
|
+
self.loaded_models.extend([m.__name__ for m in models])
|
|
55
|
+
|
|
56
|
+
logger.info(f"Loaded {len(models)} models from {config_path}")
|
|
57
|
+
return models
|
|
58
|
+
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.error(f"Failed to load config from {config_path}: {e}")
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
def _get_file_dependencies(self, config_file: Path) -> List[str]:
|
|
64
|
+
"""
|
|
65
|
+
Get dependencies from a YAML config file.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
config_file: Path to config file
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of dependency filenames (not full paths)
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
with open(config_file, 'r') as f:
|
|
75
|
+
config_data = yaml.safe_load(f)
|
|
76
|
+
|
|
77
|
+
if not config_data:
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
# Check for 'depends' or 'dependencies' key
|
|
81
|
+
depends = config_data.get('depends', config_data.get('dependencies', []))
|
|
82
|
+
|
|
83
|
+
if isinstance(depends, str):
|
|
84
|
+
depends = [depends]
|
|
85
|
+
|
|
86
|
+
return depends if isinstance(depends, list) else []
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.warning(f"Could not read dependencies from {config_file}: {e}")
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
def _topological_sort(
|
|
93
|
+
self,
|
|
94
|
+
files: List[Path],
|
|
95
|
+
config_dir: Path
|
|
96
|
+
) -> List[Path]:
|
|
97
|
+
"""
|
|
98
|
+
Sort config files based on dependencies using topological sort.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
files: List of config file paths
|
|
102
|
+
config_dir: Base directory for resolving relative dependencies
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Sorted list of config files
|
|
106
|
+
"""
|
|
107
|
+
# Build dependency graph
|
|
108
|
+
file_deps: Dict[str, List[str]] = {}
|
|
109
|
+
file_map: Dict[str, Path] = {}
|
|
110
|
+
|
|
111
|
+
for file_path in files:
|
|
112
|
+
filename = file_path.name
|
|
113
|
+
file_map[filename] = file_path
|
|
114
|
+
file_deps[filename] = self._get_file_dependencies(file_path)
|
|
115
|
+
|
|
116
|
+
# Topological sort using Kahn's algorithm
|
|
117
|
+
in_degree: Dict[str, int] = {name: 0 for name in file_map.keys()}
|
|
118
|
+
|
|
119
|
+
for filename, deps in file_deps.items():
|
|
120
|
+
for dep in deps:
|
|
121
|
+
if dep in in_degree:
|
|
122
|
+
in_degree[filename] += 1
|
|
123
|
+
|
|
124
|
+
# Queue of files with no dependencies
|
|
125
|
+
queue = [name for name, degree in in_degree.items() if degree == 0]
|
|
126
|
+
sorted_files = []
|
|
127
|
+
|
|
128
|
+
while queue:
|
|
129
|
+
# Sort queue alphabetically for deterministic ordering
|
|
130
|
+
queue.sort()
|
|
131
|
+
|
|
132
|
+
filename = queue.pop(0)
|
|
133
|
+
sorted_files.append(file_map[filename])
|
|
134
|
+
|
|
135
|
+
# Reduce in-degree for files that depend on this one
|
|
136
|
+
for other_file, deps in file_deps.items():
|
|
137
|
+
if filename in deps:
|
|
138
|
+
in_degree[other_file] -= 1
|
|
139
|
+
if in_degree[other_file] == 0:
|
|
140
|
+
queue.append(other_file)
|
|
141
|
+
|
|
142
|
+
# Check for circular dependencies
|
|
143
|
+
if len(sorted_files) != len(files):
|
|
144
|
+
remaining = set(file_map.keys()) - {f.name for f in sorted_files}
|
|
145
|
+
logger.error(f"Circular dependency detected in files: {remaining}")
|
|
146
|
+
# Fall back to alphabetical sort
|
|
147
|
+
logger.warning("Falling back to alphabetical sort")
|
|
148
|
+
return sorted(files)
|
|
149
|
+
|
|
150
|
+
logger.debug(f"File load order (dependency-sorted): {[f.name for f in sorted_files]}")
|
|
151
|
+
return sorted_files
|
|
152
|
+
|
|
153
|
+
def load_from_directory(
|
|
154
|
+
self,
|
|
155
|
+
directory_path: str,
|
|
156
|
+
pattern: str = "*.yaml",
|
|
157
|
+
recursive: bool = False,
|
|
158
|
+
use_dependencies: bool = True
|
|
159
|
+
) -> List:
|
|
160
|
+
"""
|
|
161
|
+
Load all model configs from a directory.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
directory_path: Path to directory containing config files
|
|
165
|
+
pattern: Glob pattern for config files (default: *.yaml)
|
|
166
|
+
recursive: If True, search subdirectories
|
|
167
|
+
use_dependencies: If True, sort files by dependencies (default: True)
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
List of all created model classes
|
|
171
|
+
"""
|
|
172
|
+
config_dir = Path(directory_path)
|
|
173
|
+
|
|
174
|
+
if not config_dir.exists():
|
|
175
|
+
logger.warning(f"Config directory does not exist: {directory_path}")
|
|
176
|
+
return []
|
|
177
|
+
|
|
178
|
+
logger.info(f"Loading models from directory: {directory_path}")
|
|
179
|
+
|
|
180
|
+
all_models = []
|
|
181
|
+
|
|
182
|
+
# Find all config files
|
|
183
|
+
if recursive:
|
|
184
|
+
config_files = list(config_dir.rglob(pattern))
|
|
185
|
+
else:
|
|
186
|
+
config_files = list(config_dir.glob(pattern))
|
|
187
|
+
|
|
188
|
+
if not config_files:
|
|
189
|
+
logger.warning(f"No config files found in {directory_path}")
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
# Sort files by dependencies or alphabetically
|
|
193
|
+
if use_dependencies:
|
|
194
|
+
sorted_files = self._topological_sort(config_files, config_dir)
|
|
195
|
+
else:
|
|
196
|
+
sorted_files = sorted(config_files)
|
|
197
|
+
|
|
198
|
+
logger.info(f"Loading {len(sorted_files)} config files in order:")
|
|
199
|
+
for idx, file_path in enumerate(sorted_files, 1):
|
|
200
|
+
logger.info(f" {idx}. {file_path.name}")
|
|
201
|
+
|
|
202
|
+
# Load each config file in order
|
|
203
|
+
for config_file in sorted_files:
|
|
204
|
+
try:
|
|
205
|
+
models = self.load_from_file(str(config_file))
|
|
206
|
+
all_models.extend(models)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"Error loading {config_file}: {e}")
|
|
209
|
+
# Continue loading other files rather than stopping
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
logger.info(f"Loaded {len(all_models)} total models from {directory_path}")
|
|
213
|
+
return all_models
|
|
214
|
+
|
|
215
|
+
def load_from_multiple_directories(self, directories: List[str]) -> List:
|
|
216
|
+
"""
|
|
217
|
+
Load models from multiple directories.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
directories: List of directory paths
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List of all created model classes
|
|
224
|
+
"""
|
|
225
|
+
all_models = []
|
|
226
|
+
|
|
227
|
+
for directory in directories:
|
|
228
|
+
models = self.load_from_directory(directory)
|
|
229
|
+
all_models.extend(models)
|
|
230
|
+
|
|
231
|
+
return all_models
|
|
232
|
+
|
|
233
|
+
def get_loaded_configs(self) -> List[str]:
|
|
234
|
+
"""Get list of loaded configuration file paths."""
|
|
235
|
+
return self.loaded_configs
|
|
236
|
+
|
|
237
|
+
def get_loaded_models(self) -> List[str]:
|
|
238
|
+
"""Get list of loaded model names."""
|
|
239
|
+
return self.loaded_models
|
|
240
|
+
|
|
241
|
+
def reload(self, config_path: str):
|
|
242
|
+
"""
|
|
243
|
+
Reload models from a configuration file.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
config_path: Path to config file to reload
|
|
247
|
+
"""
|
|
248
|
+
logger.info(f"Reloading models from {config_path}")
|
|
249
|
+
|
|
250
|
+
# Load the config again
|
|
251
|
+
models = self.load_from_file(config_path)
|
|
252
|
+
|
|
253
|
+
logger.info(f"Reloaded {len(models)} models from {config_path}")
|
|
254
|
+
return models
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# Singleton instance
|
|
258
|
+
model_loader = ModelConfigLoader()
|
|
File without changes
|