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.
Files changed (67) hide show
  1. common/__init__.py +329 -0
  2. common/config/__init__.py +16 -0
  3. common/config/model_loader.py +258 -0
  4. common/engine/__init__.py +0 -0
  5. common/engine/config_executor.py +317 -0
  6. common/engine/operation_registry.py +173 -0
  7. common/init.py +318 -0
  8. common/models/__init__.py +22 -0
  9. common/models/base.py +399 -0
  10. common/models/configs/__init__.py +0 -0
  11. common/models/core/__init.__.py +14 -0
  12. common/models/core/tag.py +53 -0
  13. common/models/core/user.py +299 -0
  14. common/models/factory.py +654 -0
  15. common/models/mixins.py +327 -0
  16. common/models/registry.py +267 -0
  17. common/services/__init__.py +41 -0
  18. common/services/data_fetcher/__init__.py +49 -0
  19. common/services/data_fetcher/alphavantage.py +190 -0
  20. common/services/data_fetcher/base.py +127 -0
  21. common/services/data_fetcher/curated_data.py +197 -0
  22. common/services/data_fetcher/polygon.py +244 -0
  23. common/services/data_fetcher/stock.py +268 -0
  24. common/services/data_fetcher/yfinance.py +154 -0
  25. common/services/data_processor/__init__.py +232 -0
  26. common/services/data_processor/portfolio_metrics.py +261 -0
  27. common/services/data_processor/returns.py +112 -0
  28. common/services/data_processor/risk_metrics.py +231 -0
  29. common/services/data_processor/standardization.py +129 -0
  30. common/services/data_processor/technical_indicators.py +281 -0
  31. common/services/db_service.py +268 -0
  32. common/services/encryption/__init__.py +0 -0
  33. common/services/encryption/field_encryption.py +349 -0
  34. common/services/gdpr/__init__.py +13 -0
  35. common/services/gdpr/deletion.py +433 -0
  36. common/services/inventory/__init__.py +74 -0
  37. common/services/inventory/available_capabilities.py +602 -0
  38. common/services/inventory/cached_capabilities.py +557 -0
  39. common/services/inventory/loader.py +331 -0
  40. common/services/persistence/__init__.py +94 -0
  41. common/services/persistence/base.py +300 -0
  42. common/services/persistence/cache.py +405 -0
  43. common/services/persistence/instances.py +109 -0
  44. common/services/persistence/manager.py +218 -0
  45. common/services/persistence/notification.py +400 -0
  46. common/services/persistence/research.py +331 -0
  47. common/services/persistence/session.py +690 -0
  48. common/services/persistence/tracker.py +434 -0
  49. common/services/security/__init__.py +0 -0
  50. common/services/security/oauth_service.py +806 -0
  51. common/services/vector/__init__.py +23 -0
  52. common/services/vector/auto_ingestion.py +452 -0
  53. common/services/vector/tag_embedding.py +394 -0
  54. common/utils/__init__.py +57 -0
  55. common/utils/config.py +296 -0
  56. common/utils/db_utils.py +119 -0
  57. common/utils/helpers.py +151 -0
  58. common/utils/time_helpers.py +6 -0
  59. common/utils/validators.py +211 -0
  60. modules/__init__.py +0 -0
  61. modules/config/custom_analysis.yaml +30 -0
  62. modules/config/investing_operations_config.yaml +182 -0
  63. modules/investing_operations.py +431 -0
  64. timber_common-0.2.9.dist-info/METADATA +651 -0
  65. timber_common-0.2.9.dist-info/RECORD +67 -0
  66. timber_common-0.2.9.dist-info/WHEEL +4 -0
  67. 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