masworks 0.1.0.dev20260205__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.
- masworks/MasDatabase/__init__.py +37 -0
- masworks/MasDatabase/base.py +116 -0
- masworks/MasDatabase/context.py +58 -0
- masworks/MasDatabase/decorators.py +37 -0
- masworks/MasDatabase/generic_session.py +228 -0
- masworks/MasDatabase/models.py +351 -0
- masworks/MasDatabase/operations.py +453 -0
- masworks/MasDatabase/time_series_session.py +512 -0
- masworks/MasDatabase/utilities.py +119 -0
- masworks/MasDatabase/versioned_models.py +876 -0
- masworks/MasDatabase/versioned_session.py +1043 -0
- masworks/__init__.py +28 -0
- masworks/analysis/__init__.py +14 -0
- masworks/analysis/backtesting.py +319 -0
- masworks/analysis/modeling.py +294 -0
- masworks/config.py +242 -0
- masworks/constants.py +49 -0
- masworks/core.py +67 -0
- masworks/enhanced_time_series_reader_sqlalchemy.py +737 -0
- masworks/feeds/__init__.py +23 -0
- masworks/feeds/base.py +110 -0
- masworks/feeds/bridge.py +1320 -0
- masworks/feeds/bridge_server_filter.py +171 -0
- masworks/feeds/bridge_service_manager.py +414 -0
- masworks/feeds/datastream.py +555 -0
- masworks/feeds/factory.py +135 -0
- masworks/feeds/fred.py +271 -0
- masworks/logging_config.py +139 -0
- masworks/mas_time_series_admin.py +1517 -0
- masworks/mas_time_series_base.py +40 -0
- masworks/mas_time_series_reader.py +857 -0
- masworks/test_versioned_models.py +86 -0
- masworks/types.py +20 -0
- masworks/utils/__init__.py +53 -0
- masworks/utils/bbg_ticker_detail.py +429 -0
- masworks/utils/data_utils.py +774 -0
- masworks/utils/database_connection.py +172 -0
- masworks/utils/database_operations.py +791 -0
- masworks/utils/date_utils.py +104 -0
- masworks/utils/general.py +348 -0
- masworks/utils/model_utils.py +41 -0
- masworks/utils/series_utils.py +133 -0
- masworks/utils/session_base.py +166 -0
- masworks/utils/time_series.py +323 -0
- masworks/versioned_enhanced_reader.py +537 -0
- masworks/versioned_time_series_admin.py +1166 -0
- masworks/versioned_time_series_base.py +359 -0
- masworks/versioned_time_series_reader.py +690 -0
- masworks-0.1.0.dev20260205.dist-info/METADATA +94 -0
- masworks-0.1.0.dev20260205.dist-info/RECORD +52 -0
- masworks-0.1.0.dev20260205.dist-info/WHEEL +5 -0
- masworks-0.1.0.dev20260205.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MasDatabase module for MasWorks library.
|
|
3
|
+
|
|
4
|
+
Provides MasWorks-specific database functionality including session management
|
|
5
|
+
and ORM models for time series data. This module contains domain-specific
|
|
6
|
+
database operations, while generic database utilities are in utils/.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ..utils.database_connection import DatabaseManager
|
|
10
|
+
from .models import TimeSeriesData, TimeSeriesMeta, TimeSeriesSelector, RefData, TickerMapping, TimeSeriesScheduler
|
|
11
|
+
# Old SessionManager moved to recycle/ - use TimeSeriesSessionManager instead
|
|
12
|
+
from .generic_session import GenericSessionManager
|
|
13
|
+
from .time_series_session import TimeSeriesSessionManager
|
|
14
|
+
from .base import DatabaseBase
|
|
15
|
+
from .operations import SeriesOperationMixin, BloombergOperationsMixin
|
|
16
|
+
from .decorators import handle_series_operation_errors
|
|
17
|
+
from .utilities import update_threshold_daily_close, create_update_filter, parse_time_string
|
|
18
|
+
# TimeSeriesContext imported on demand to avoid circular imports
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"DatabaseManager",
|
|
22
|
+
"GenericSessionManager",
|
|
23
|
+
"TimeSeriesSessionManager",
|
|
24
|
+
"TimeSeriesData",
|
|
25
|
+
"TimeSeriesMeta",
|
|
26
|
+
"TimeSeriesSelector",
|
|
27
|
+
"RefData",
|
|
28
|
+
"TickerMapping",
|
|
29
|
+
"TimeSeriesScheduler",
|
|
30
|
+
"DatabaseBase",
|
|
31
|
+
"SeriesOperationMixin",
|
|
32
|
+
"BloombergOperationsMixin",
|
|
33
|
+
"handle_series_operation_errors",
|
|
34
|
+
"update_threshold_daily_close",
|
|
35
|
+
"create_update_filter",
|
|
36
|
+
"parse_time_string",
|
|
37
|
+
]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database base functionality for MasWorks library.
|
|
3
|
+
|
|
4
|
+
Provides shared database infrastructure including connection management,
|
|
5
|
+
feed initialization, and common utilities used by both reader and admin classes.
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, Any
|
|
9
|
+
|
|
10
|
+
from masworks.config import Config
|
|
11
|
+
from masworks.utils.database_connection import DatabaseManager
|
|
12
|
+
from .time_series_session import TimeSeriesSessionManager
|
|
13
|
+
from masworks.feeds import BridgeFeed, FredFeed, DatastreamFeed
|
|
14
|
+
from masworks.constants import SOURCE_ALIASES
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DatabaseBase:
|
|
20
|
+
"""
|
|
21
|
+
Base class for database operations in MasWorks.
|
|
22
|
+
|
|
23
|
+
Provides shared infrastructure including database connections and common utilities.
|
|
24
|
+
This class is not meant to be used directly - use MasTimeSeriesReader for read
|
|
25
|
+
operations or MasTimeSeriesAdmin for write operations.
|
|
26
|
+
|
|
27
|
+
Note: Feed operations are only available in MasTimeSeriesAdmin.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, config: Config, auto_create_tables: bool = True):
|
|
31
|
+
"""
|
|
32
|
+
Initialize DatabaseBase.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
config: Configuration object with database and feed settings
|
|
36
|
+
auto_create_tables: If True, automatically create database tables if they don't exist
|
|
37
|
+
"""
|
|
38
|
+
self.config = config
|
|
39
|
+
|
|
40
|
+
# Initialize database components
|
|
41
|
+
self.db_manager = DatabaseManager(config.database)
|
|
42
|
+
|
|
43
|
+
# Check if this is a versioned class
|
|
44
|
+
# Versioned classes should use create_version() for versioned tables,
|
|
45
|
+
# not auto-create current tables
|
|
46
|
+
is_versioned = self._is_versioned_class()
|
|
47
|
+
|
|
48
|
+
# Create tables if requested and they don't exist
|
|
49
|
+
# Skip for versioned classes (they should use create_version() explicitly)
|
|
50
|
+
if auto_create_tables and not is_versioned:
|
|
51
|
+
self.db_manager.create_tables_if_not_exist()
|
|
52
|
+
|
|
53
|
+
# Session manager will be attached by subclasses as needed
|
|
54
|
+
self.session_manager = None
|
|
55
|
+
|
|
56
|
+
def _is_versioned_class(self) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Check if this instance is a versioned class.
|
|
59
|
+
|
|
60
|
+
Uses lazy import to avoid circular dependency issues.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if this is a VersionedTimeSeriesBase subclass, False otherwise
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
# Lazy import to avoid circular dependency
|
|
67
|
+
# Use absolute import path to avoid issues
|
|
68
|
+
from masworks.versioned_time_series_base import VersionedTimeSeriesBase
|
|
69
|
+
return isinstance(self, VersionedTimeSeriesBase)
|
|
70
|
+
except (ImportError, AttributeError):
|
|
71
|
+
# Fallback: check class name if import fails
|
|
72
|
+
# This handles edge cases but shouldn't normally be needed
|
|
73
|
+
class_name = self.__class__.__name__
|
|
74
|
+
return 'Versioned' in class_name and 'TimeSeries' in class_name
|
|
75
|
+
|
|
76
|
+
# Note: Feed initialization moved to TimeSeriesAdmin
|
|
77
|
+
# Feeds are only needed for admin operations, not read-only operations
|
|
78
|
+
|
|
79
|
+
def test_connection(self) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Test database connection.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if connection is successful, False otherwise
|
|
85
|
+
"""
|
|
86
|
+
return self.db_manager.test_connection()
|
|
87
|
+
|
|
88
|
+
def create_tables(self) -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Manually create all database tables.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if tables were created successfully, False otherwise
|
|
94
|
+
"""
|
|
95
|
+
return self.db_manager.create_tables()
|
|
96
|
+
|
|
97
|
+
def create_tables_if_not_exist(self) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Create database tables only if they don't already exist.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if tables exist or were created successfully, False otherwise
|
|
103
|
+
"""
|
|
104
|
+
return self.db_manager.create_tables_if_not_exist()
|
|
105
|
+
|
|
106
|
+
def close(self):
|
|
107
|
+
"""
|
|
108
|
+
Close database connections and cleanup resources.
|
|
109
|
+
|
|
110
|
+
Should be called when done with the instance to properly cleanup resources.
|
|
111
|
+
"""
|
|
112
|
+
self.db_manager.close()
|
|
113
|
+
|
|
114
|
+
# Note: Feed-related methods (get_available_feeds, get_feed_info)
|
|
115
|
+
# have been moved to TimeSeriesAdmin where feeds are actually available.
|
|
116
|
+
# DatabaseBase only provides database operations, not feed operations.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context manager for MasWorks time series operations.
|
|
3
|
+
|
|
4
|
+
This module provides TimeSeriesContext which is a convenient context manager
|
|
5
|
+
for time series operations, specifically designed for MasWorks time series
|
|
6
|
+
database operations.
|
|
7
|
+
"""
|
|
8
|
+
from typing import Optional, Union
|
|
9
|
+
|
|
10
|
+
from masworks.config import Config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TimeSeriesContext:
|
|
14
|
+
"""
|
|
15
|
+
Context manager wrapper for time series operations.
|
|
16
|
+
|
|
17
|
+
Provides a convenient way to use TimeSeriesReader or TimeSeriesAdmin with automatic
|
|
18
|
+
resource cleanup using Python's 'with' statement.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
# For read operations
|
|
22
|
+
with TimeSeriesContext(config, mode="reader") as reader:
|
|
23
|
+
data = reader.get_series_data(ticker='SPX Index', field='PX_LAST')
|
|
24
|
+
|
|
25
|
+
# For admin operations
|
|
26
|
+
with TimeSeriesContext(config, mode="admin") as admin:
|
|
27
|
+
result = admin.upsert_series('SPX Index', 'PX_LAST')
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, config: Config, mode: str = "admin"):
|
|
31
|
+
"""
|
|
32
|
+
Initialize TimeSeriesContext.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
config: Configuration object with database and feed settings
|
|
36
|
+
mode: Operation mode - "reader" for read-only, "admin" for full access
|
|
37
|
+
"""
|
|
38
|
+
self.config = config
|
|
39
|
+
self.mode = mode
|
|
40
|
+
self._instance: Optional[Union['MasTimeSeriesReader', 'MasTimeSeriesAdmin']] = None
|
|
41
|
+
|
|
42
|
+
def __enter__(self) -> Union['MasTimeSeriesReader', 'MasTimeSeriesAdmin']:
|
|
43
|
+
"""Enter context manager and initialize the appropriate class."""
|
|
44
|
+
# Import here to avoid circular imports
|
|
45
|
+
from masworks.mas_time_series_reader import MasTimeSeriesReader
|
|
46
|
+
from masworks.mas_time_series_admin import MasTimeSeriesAdmin
|
|
47
|
+
|
|
48
|
+
if self.mode == "reader":
|
|
49
|
+
self._instance = MasTimeSeriesReader(self.config)
|
|
50
|
+
else:
|
|
51
|
+
self._instance = MasTimeSeriesAdmin(self.config)
|
|
52
|
+
return self._instance
|
|
53
|
+
|
|
54
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
|
55
|
+
"""Exit context manager and cleanup resources."""
|
|
56
|
+
if self._instance:
|
|
57
|
+
self._instance.close()
|
|
58
|
+
return False
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database decorators for MasWorks library.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
from ..feeds import DataFeedError
|
|
9
|
+
from ..constants import STATUS_CODES
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def handle_series_operation_errors(func: Callable) -> Callable:
|
|
15
|
+
"""
|
|
16
|
+
Decorator to standardize error handling for series operations.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
func: The function to wrap
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Wrapped function with error handling
|
|
23
|
+
"""
|
|
24
|
+
@wraps(func)
|
|
25
|
+
def wrapper(self, *args, **kwargs) -> Any:
|
|
26
|
+
try:
|
|
27
|
+
return func(self, *args, **kwargs)
|
|
28
|
+
except DataFeedError as e:
|
|
29
|
+
ticker = kwargs.get('ticker', args[0] if args else 'unknown')
|
|
30
|
+
logger.error(f"Data feed error for {ticker}: {e}")
|
|
31
|
+
return {"status": STATUS_CODES['FEED_ERROR'], "error": str(e)}
|
|
32
|
+
except Exception as e:
|
|
33
|
+
ticker = kwargs.get('ticker', args[0] if args else 'unknown')
|
|
34
|
+
operation = func.__name__.replace('_series', '')
|
|
35
|
+
logger.error(f"{operation.title()} failed for {ticker}: {e}")
|
|
36
|
+
return {"status": STATUS_CODES['ERROR'], "error": str(e)}
|
|
37
|
+
return wrapper
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generic session management for database operations.
|
|
3
|
+
|
|
4
|
+
This module provides a generic SessionManager that can work with any database
|
|
5
|
+
schema and table structure, without hardcoded dependencies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Optional, List, Dict, Any
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from sqlalchemy import text
|
|
12
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
|
|
15
|
+
from ..utils.session_base import BaseSessionManager
|
|
16
|
+
from ..utils.database_connection import DatabaseManager
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GenericSessionManager(BaseSessionManager):
|
|
22
|
+
"""
|
|
23
|
+
Generic session manager for database operations.
|
|
24
|
+
|
|
25
|
+
Provides generic database session management that can work with any
|
|
26
|
+
database schema and table structure without hardcoded dependencies.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, db_manager: DatabaseManager, chunk_size: int = 5000):
|
|
30
|
+
"""
|
|
31
|
+
Initialize GenericSessionManager.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
db_manager: DatabaseManager instance
|
|
35
|
+
chunk_size: Size of chunks for bulk operations
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(db_manager, chunk_size)
|
|
38
|
+
# Track temp tables for reuse within session
|
|
39
|
+
self._temp_tables = {} # {table_name: connection_id}
|
|
40
|
+
self._conn = None
|
|
41
|
+
|
|
42
|
+
def _get_connection(self):
|
|
43
|
+
"""
|
|
44
|
+
Get the current connection or create a new one.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Database connection object
|
|
48
|
+
"""
|
|
49
|
+
if self._conn is None:
|
|
50
|
+
self._conn = self.db_manager.engine.connect()
|
|
51
|
+
return self._conn
|
|
52
|
+
|
|
53
|
+
def _close_connection(self):
|
|
54
|
+
"""Close the current connection if it exists."""
|
|
55
|
+
if self._conn is not None:
|
|
56
|
+
self._conn.close()
|
|
57
|
+
self._conn = None
|
|
58
|
+
|
|
59
|
+
def _get_schema(self) -> str:
|
|
60
|
+
"""Get the database schema from config."""
|
|
61
|
+
return self.db_manager.config.schema
|
|
62
|
+
|
|
63
|
+
def _get_or_create_temp_table(self, temp_table: str, source_table: str = None, schema: str = None) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Get existing temp table or create new one for reuse within session.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
temp_table: Name of temp table to create/reuse
|
|
69
|
+
source_table: Source table to copy structure from (if None, defaults to "TS_Data")
|
|
70
|
+
schema: Database schema name (if None, uses config schema)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Name of temp table to use
|
|
74
|
+
"""
|
|
75
|
+
# Generate temp table name if not provided
|
|
76
|
+
if temp_table is None:
|
|
77
|
+
import time
|
|
78
|
+
import random
|
|
79
|
+
timestamp = int(time.time() * 1000) % 10000
|
|
80
|
+
random_suffix = random.randint(10, 99)
|
|
81
|
+
temp_table = f"#tmp_{timestamp}_{random_suffix}"
|
|
82
|
+
|
|
83
|
+
# Get connection
|
|
84
|
+
conn = self._get_connection()
|
|
85
|
+
|
|
86
|
+
# Get connection ID for tracking
|
|
87
|
+
conn_id = id(conn)
|
|
88
|
+
|
|
89
|
+
# Check if we can reuse an existing temp table
|
|
90
|
+
if temp_table in self._temp_tables and self._temp_tables[temp_table] == conn_id:
|
|
91
|
+
# Reuse existing temp table - just truncate it
|
|
92
|
+
try:
|
|
93
|
+
conn.execute(text(f"TRUNCATE TABLE {temp_table}"))
|
|
94
|
+
logger.debug(f"Reusing existing temp table: {temp_table}")
|
|
95
|
+
return temp_table
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.warning(f"Failed to truncate temp table {temp_table}, will recreate: {e}")
|
|
98
|
+
# Fall through to create new table
|
|
99
|
+
|
|
100
|
+
# Get table name and schema from model if not provided
|
|
101
|
+
if source_table is None or schema is None:
|
|
102
|
+
# Default to TimeSeriesData table info, but allow override
|
|
103
|
+
if source_table is None:
|
|
104
|
+
source_table = "TS_Data" # Default table name
|
|
105
|
+
|
|
106
|
+
if schema is None:
|
|
107
|
+
schema = self._get_schema() # Use config schema
|
|
108
|
+
|
|
109
|
+
# Create new temp table
|
|
110
|
+
self.create_temp_table(conn, temp_table, source_table, schema)
|
|
111
|
+
self._temp_tables[temp_table] = conn_id
|
|
112
|
+
logger.debug(f"Created new temp table: {temp_table}")
|
|
113
|
+
return temp_table
|
|
114
|
+
|
|
115
|
+
def _get_or_create_temp_table_from_model(self, temp_table: str, model_class) -> str:
|
|
116
|
+
"""
|
|
117
|
+
Get existing temp table or create new one using SQLAlchemy model information.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
temp_table: Name of temp table to create/reuse
|
|
121
|
+
model_class: SQLAlchemy model class to get table name and schema from
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Name of temp table to use
|
|
125
|
+
"""
|
|
126
|
+
# Extract table name and schema from model
|
|
127
|
+
source_table = model_class.__tablename__
|
|
128
|
+
schema = model_class.__table_args__["schema"]
|
|
129
|
+
|
|
130
|
+
# Use the main method with extracted parameters
|
|
131
|
+
return self._get_or_create_temp_table(temp_table, source_table, schema)
|
|
132
|
+
|
|
133
|
+
def _cleanup_temp_table(self, temp_table: str):
|
|
134
|
+
"""
|
|
135
|
+
Clean up temp table, but keep it for reuse if possible.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
temp_table: Name of temp table to clean up
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
# Get connection
|
|
142
|
+
conn = self._get_connection()
|
|
143
|
+
|
|
144
|
+
# Debug: Check connection type
|
|
145
|
+
logger.debug(f"Cleaning up temp table {temp_table}, connection type: {type(conn)}")
|
|
146
|
+
|
|
147
|
+
# Check if connection has execute method
|
|
148
|
+
if not hasattr(conn, 'execute'):
|
|
149
|
+
logger.warning(f"Connection object {type(conn)} does not have execute method, skipping cleanup for {temp_table}")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# Try to truncate first (for reuse)
|
|
153
|
+
conn.execute(text(f"TRUNCATE TABLE {temp_table}"))
|
|
154
|
+
logger.debug(f"Truncated temp table for reuse: {temp_table}")
|
|
155
|
+
except Exception as e:
|
|
156
|
+
# If truncate fails, try to drop
|
|
157
|
+
try:
|
|
158
|
+
logger.debug(f"Truncate failed for {temp_table}: {e}, trying drop")
|
|
159
|
+
conn.execute(text(f"DROP TABLE {temp_table}"))
|
|
160
|
+
logger.debug(f"Dropped temp table: {temp_table}")
|
|
161
|
+
# Remove from tracking
|
|
162
|
+
if temp_table in self._temp_tables:
|
|
163
|
+
del self._temp_tables[temp_table]
|
|
164
|
+
except Exception as drop_error:
|
|
165
|
+
logger.warning(f"Could not clean up {temp_table}: {drop_error}")
|
|
166
|
+
|
|
167
|
+
def _cleanup_all_temp_tables(self):
|
|
168
|
+
"""
|
|
169
|
+
Clean up all temp tables for the current connection.
|
|
170
|
+
"""
|
|
171
|
+
# Get connection
|
|
172
|
+
conn = self._get_connection()
|
|
173
|
+
conn_id = id(conn)
|
|
174
|
+
|
|
175
|
+
tables_to_remove = []
|
|
176
|
+
|
|
177
|
+
for temp_table, tracked_conn_id in self._temp_tables.items():
|
|
178
|
+
if tracked_conn_id == conn_id:
|
|
179
|
+
try:
|
|
180
|
+
conn.execute(text(f"DROP TABLE {temp_table}"))
|
|
181
|
+
logger.debug(f"Dropped temp table: {temp_table}")
|
|
182
|
+
tables_to_remove.append(temp_table)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.warning(f"Failed to clean up temp table {temp_table}: {e}")
|
|
185
|
+
|
|
186
|
+
# Remove from tracking
|
|
187
|
+
for temp_table in tables_to_remove:
|
|
188
|
+
del self._temp_tables[temp_table]
|
|
189
|
+
|
|
190
|
+
@contextmanager
|
|
191
|
+
def bulk_operations_context(self):
|
|
192
|
+
"""
|
|
193
|
+
Context manager for bulk operations that reuses connections and temp tables.
|
|
194
|
+
|
|
195
|
+
This reuses an existing connection if available, or creates a new one and keeps it open.
|
|
196
|
+
Optimized for sequences of operations where you want to reuse temp tables for better performance.
|
|
197
|
+
|
|
198
|
+
Example:
|
|
199
|
+
with session_manager.bulk_operations_context() as conn:
|
|
200
|
+
result1 = session_manager.some_operation(df1, conn=conn)
|
|
201
|
+
result2 = session_manager.some_operation(df2, conn=conn)
|
|
202
|
+
"""
|
|
203
|
+
# If we already have a connection, use it
|
|
204
|
+
if self._conn is not None:
|
|
205
|
+
yield self._conn
|
|
206
|
+
else:
|
|
207
|
+
# Create new connection and keep it open
|
|
208
|
+
conn = self.db_manager.engine.connect()
|
|
209
|
+
try:
|
|
210
|
+
self._conn = conn
|
|
211
|
+
yield conn
|
|
212
|
+
finally:
|
|
213
|
+
# Don't close - let it stay in self._conn for future use
|
|
214
|
+
# Just clean up temp tables
|
|
215
|
+
self._cleanup_all_temp_tables()
|
|
216
|
+
|
|
217
|
+
def close(self):
|
|
218
|
+
"""Close database connections and cleanup resources."""
|
|
219
|
+
# Clean up any remaining temp tables
|
|
220
|
+
if hasattr(self, '_temp_tables'):
|
|
221
|
+
self._cleanup_all_temp_tables()
|
|
222
|
+
self._temp_tables.clear()
|
|
223
|
+
|
|
224
|
+
# Close current connection
|
|
225
|
+
self._close_connection()
|
|
226
|
+
|
|
227
|
+
# Close base class resources
|
|
228
|
+
super().close()
|