timber-common 0.1.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.
Files changed (67) hide show
  1. common/__init__.py +327 -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 +244 -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 +196 -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 +676 -0
  48. common/services/persistence/tracker.py +434 -0
  49. common/services/security/__init__.py +0 -0
  50. common/services/security/oauth_service.py +316 -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.1.0.dist-info/METADATA +651 -0
  65. timber_common-0.1.0.dist-info/RECORD +67 -0
  66. timber_common-0.1.0.dist-info/WHEEL +4 -0
  67. timber_common-0.1.0.dist-info/licenses/LICENSE +59 -0
common/__init__.py ADDED
@@ -0,0 +1,327 @@
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
+ )
47
+
48
+ # Import config instance and class
49
+ from .utils.config import config, Config
50
+ from .config.model_loader import model_loader, ModelConfigLoader
51
+
52
+ from .models.base import Base, db_manager
53
+ from .models.registry import (
54
+ model_registry,
55
+ register_model,
56
+ get_model,
57
+ get_session_model
58
+ )
59
+ from .models.factory import model_factory
60
+
61
+ from .models.mixins import (
62
+ TimestampMixin,
63
+ SoftDeleteMixin,
64
+ EncryptedFieldMixin,
65
+ GDPRComplianceMixin,
66
+ SearchableMixin,
67
+ CacheableMixin,
68
+ AuditMixin
69
+ )
70
+
71
+ # =============================================================================
72
+ # CORE SERVICES
73
+ # =============================================================================
74
+
75
+ # These are your existing services from the old architecture
76
+ # Import them conditionally to avoid breaking existing code
77
+
78
+ try:
79
+ from common.services.data_fetcher import stock_data_service, StockDataService
80
+ STOCK_DATA_AVAILABLE = True
81
+ except ImportError:
82
+ STOCK_DATA_AVAILABLE = False
83
+ stock_data_service = None
84
+ StockDataService = None
85
+
86
+ try:
87
+ from common.services.data_fetcher import curated_data_loader, CuratedDataLoader
88
+ CURATED_DATA_AVAILABLE = True
89
+ except ImportError:
90
+ CURATED_DATA_AVAILABLE = False
91
+ curated_data_loader = None
92
+ CuratedDataLoader = None
93
+
94
+ try:
95
+ from .services.db_service import db_service, DBService, get_db
96
+ DB_SERVICE_AVAILABLE = True
97
+ except ImportError:
98
+ # Fall back to new db_manager
99
+ DB_SERVICE_AVAILABLE = False
100
+ db_service = None
101
+ DBService = None
102
+ # Use new architecture's get_db
103
+ get_db = db_manager.get_db_session
104
+
105
+ # =============================================================================
106
+ # UTILITIES
107
+ # =============================================================================
108
+
109
+ try:
110
+ from .utils.helpers import (
111
+ parse_natural_period_to_dates,
112
+ standardize_symbol,
113
+ format_currency,
114
+ )
115
+ HELPERS_AVAILABLE = True
116
+ except ImportError:
117
+ HELPERS_AVAILABLE = False
118
+ parse_natural_period_to_dates = None
119
+ standardize_symbol = None
120
+ format_currency = None
121
+
122
+ try:
123
+ from .utils.validators import (
124
+ validate_stock_symbol,
125
+ validate_date_string,
126
+ validate_date_range,
127
+ validate_dataframe,
128
+ validate_price_data,
129
+ )
130
+ VALIDATORS_AVAILABLE = True
131
+ except ImportError:
132
+ VALIDATORS_AVAILABLE = False
133
+ validate_stock_symbol = None
134
+ validate_date_string = None
135
+ validate_date_range = None
136
+ validate_dataframe = None
137
+ validate_price_data = None
138
+
139
+ # =============================================================================
140
+ # SUBMODULES
141
+ # =============================================================================
142
+
143
+ from . import models
144
+ from . import schemas
145
+
146
+ # =============================================================================
147
+ # PUBLIC API
148
+ # =============================================================================
149
+
150
+ __all__ = [
151
+ # Version
152
+ '__version__',
153
+
154
+ # ===== NEW ARCHITECTURE =====
155
+
156
+ # Initialization
157
+ 'initialize_timber',
158
+ 'shutdown_timber',
159
+ 'get_initialization_status',
160
+
161
+ # Configuration
162
+ 'config',
163
+ 'Config',
164
+ 'model_loader',
165
+ 'ModelConfigLoader',
166
+
167
+ # Database
168
+ 'Base',
169
+ 'db_manager',
170
+ 'get_db',
171
+
172
+ # Models
173
+ 'model_registry',
174
+ 'register_model',
175
+ 'get_model',
176
+ 'get_session_model',
177
+ 'model_factory',
178
+
179
+ # Mixins
180
+ 'TimestampMixin',
181
+ 'SoftDeleteMixin',
182
+ 'EncryptedFieldMixin',
183
+ 'GDPRComplianceMixin',
184
+ 'SearchableMixin',
185
+ 'CacheableMixin',
186
+ 'AuditMixin',
187
+
188
+ # ===== CORE SERVICES =====
189
+
190
+ # Stock data service
191
+ 'stock_data_service',
192
+ 'StockDataService',
193
+
194
+ # Curated data service
195
+ 'curated_data_loader',
196
+ 'CuratedDataLoader',
197
+
198
+ # DB service (legacy)
199
+ 'db_service',
200
+ 'DBService',
201
+
202
+ # ===== UTILITIES =====
203
+
204
+ # Helpers
205
+ 'parse_natural_period_to_dates',
206
+ 'standardize_symbol',
207
+ 'format_currency',
208
+
209
+ # Validators
210
+ 'validate_stock_symbol',
211
+ 'validate_date_string',
212
+ 'validate_date_range',
213
+ 'validate_dataframe',
214
+ 'validate_price_data',
215
+
216
+ # ===== SUBMODULES =====
217
+
218
+ 'models',
219
+ 'schemas',
220
+ ]
221
+
222
+
223
+ # =============================================================================
224
+ # CONVENIENCE FUNCTIONS
225
+ # =============================================================================
226
+
227
+ def get_version() -> str:
228
+ """Return the current version of timber_common."""
229
+ return __version__
230
+
231
+
232
+ def get_available_features() -> dict:
233
+ """
234
+ Get information about available features.
235
+
236
+ Returns:
237
+ Dictionary showing which features are available
238
+ """
239
+ return {
240
+ 'version': __version__,
241
+ 'new_architecture': {
242
+ 'models': True,
243
+ 'persistence': True,
244
+ 'encryption': True,
245
+ 'gdpr': True,
246
+ 'vector_search': True,
247
+ },
248
+ 'legacy_services': {
249
+ 'stock_data_service': STOCK_DATA_AVAILABLE,
250
+ 'curated_data_loader': CURATED_DATA_AVAILABLE,
251
+ 'db_service': DB_SERVICE_AVAILABLE,
252
+ },
253
+ 'utilities': {
254
+ 'helpers': HELPERS_AVAILABLE,
255
+ 'validators': VALIDATORS_AVAILABLE,
256
+ }
257
+ }
258
+
259
+
260
+ def validate_configuration() -> dict:
261
+ """
262
+ Validate the current configuration.
263
+
264
+ Returns:
265
+ Dictionary with validation results
266
+ """
267
+ results = {
268
+ 'new_architecture': True,
269
+ 'database_url': config.DATABASE_URL is not None,
270
+ 'encryption_key': config.ENCRYPTION_KEY is not None,
271
+ }
272
+
273
+ # Test database connection if initialized
274
+ if db_manager._engine:
275
+ results['database_connected'] = db_manager.check_connection()
276
+ else:
277
+ results['database_connected'] = None
278
+ results['note'] = 'Database not initialized. Call initialize_timber() first.'
279
+
280
+ # Check legacy services
281
+ if DB_SERVICE_AVAILABLE and db_service:
282
+ try:
283
+ results['legacy_db_healthy'] = db_service.health_check()
284
+ except Exception as e:
285
+ results['legacy_db_healthy'] = False
286
+ results['legacy_db_error'] = str(e)
287
+
288
+ return results
289
+
290
+
291
+ def print_status():
292
+ """Print current Timber status to console."""
293
+ print("=" * 60)
294
+ print("Timber Common Library Status")
295
+ print("=" * 60)
296
+
297
+ features = get_available_features()
298
+
299
+ print(f"\nVersion: {features['version']}")
300
+
301
+ print("\n🆕 New Architecture:")
302
+ for feature, available in features['new_architecture'].items():
303
+ status = "✓" if available else "✗"
304
+ print(f" {status} {feature}")
305
+
306
+ print("\n📦 Legacy Services:")
307
+ for service, available in features['legacy_services'].items():
308
+ status = "✓" if available else "✗"
309
+ print(f" {status} {service}")
310
+
311
+ print("\n🔧 Utilities:")
312
+ for util, available in features['utilities'].items():
313
+ status = "✓" if available else "✗"
314
+ print(f" {status} {util}")
315
+
316
+ if db_manager._engine:
317
+ status = get_initialization_status()
318
+ print(f"\n📊 Database:")
319
+ print(f" Models registered: {status['models_registered']}")
320
+ print(f" Session types: {status['session_types']}")
321
+ print(f" Tables: {status['database_tables']}")
322
+ print(f" Connected: {status['database_connected']}")
323
+ else:
324
+ print(f"\n📊 Database: Not initialized")
325
+ print(f" Call initialize_timber() to set up")
326
+
327
+ 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