BackcastPro 0.3.4__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.
@@ -0,0 +1,283 @@
1
+ import os
2
+ import pandas as pd
3
+ import duckdb
4
+ import logging
5
+ import inspect
6
+ import tempfile
7
+
8
+ logger = logging.getLogger(__name__)
9
+ logger.setLevel(logging.INFO)
10
+
11
+
12
+ class db_manager:
13
+ """
14
+ データベース管理クラス
15
+ キャッシュやデータの保存・読み込みを担当
16
+ """
17
+
18
+ def __init__(self):
19
+ # 1. 環境変数をチェック
20
+ env_cache_dir = os.environ.get('BACKCASTPRO_CACHE_DIR', tempfile.mkdtemp())
21
+ self.cache_dir = os.path.abspath(env_cache_dir)
22
+ os.environ['BACKCASTPRO_CACHE_DIR'] = self.cache_dir
23
+
24
+ # 3. ディレクトリが存在しない場合は作成
25
+ if not os.path.exists(self.cache_dir):
26
+ try:
27
+ os.makedirs(self.cache_dir, exist_ok=True)
28
+ logger.info(f"キャッシュディレクトリを作成しました: {self.cache_dir}")
29
+ self.isEnable = True
30
+ except Exception as e:
31
+ logger.warning(f"キャッシュディレクトリの作成に失敗しました: {self.cache_dir}, エラー: {e}")
32
+ self.isEnable = False
33
+ else:
34
+ self.isEnable = True
35
+
36
+ # デバッグ情報をログ出力
37
+ logger.info(f"キャッシュディレクトリ: {self.cache_dir}")
38
+ logger.info(f"キャッシュディレクトリ存在チェック: {self.isEnable}")
39
+ if not self.isEnable:
40
+ logger.warning(f"キャッシュディレクトリが存在しないか、アクセスできません: {self.cache_dir}")
41
+
42
+
43
+ def _table_exists(self, db_connection: duckdb.DuckDBPyConnection, table_name: str) -> bool:
44
+ """テーブルが存在するかチェック"""
45
+ try:
46
+ result = db_connection.execute(
47
+ "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = ?",
48
+ [table_name]
49
+ ).fetchone()
50
+ return result[0] > 0
51
+ except:
52
+ return False
53
+
54
+
55
+ def _create_table_from_dataframe(self,
56
+ db_connection: duckdb.DuckDBPyConnection,
57
+ table_name: str,
58
+ df: pd.DataFrame,
59
+ primary_keys: list = None) -> None:
60
+ """
61
+ DataFrameの構造に基づいて動的にテーブルを作成
62
+
63
+ Args:
64
+ table_name (str): 作成するテーブル名
65
+ df (pd.DataFrame): テーブル構造の基準となるDataFrame
66
+ primary_keys (list): プライマリキーとするカラム名のリスト
67
+ """
68
+ if df is None or df.empty:
69
+ raise ValueError("DataFrameが空です。テーブル構造を決定できません。")
70
+
71
+ # カラム名とデータ型を取得
72
+ columns = []
73
+ for col in df.columns:
74
+ # データ型を推定してSQL型に変換
75
+ dtype = df[col].dtype
76
+ if pd.api.types.is_integer_dtype(dtype):
77
+ sql_type = "BIGINT"
78
+ elif pd.api.types.is_float_dtype(dtype):
79
+ sql_type = "DOUBLE"
80
+ elif pd.api.types.is_datetime64_any_dtype(dtype):
81
+ sql_type = "TIMESTAMP"
82
+ elif pd.api.types.is_bool_dtype(dtype):
83
+ sql_type = "BOOLEAN"
84
+ else:
85
+ # 文字列型の場合、最大長を推定
86
+ max_length = df[col].astype(str).str.len().max()
87
+ if pd.isna(max_length) or max_length == 0:
88
+ max_length = 255
89
+ else:
90
+ max_length = min(max_length * 2, 1000) # 余裕を持たせる
91
+ sql_type = f"VARCHAR({max_length})"
92
+
93
+ # 列名はダブルクオートで明示して大文字小文字を保持
94
+ columns.append(f'"{col}" {sql_type}')
95
+
96
+ # プライマリキーの設定
97
+ if primary_keys:
98
+ # PK も列名をダブルクオートで明示
99
+ pk_columns = ", ".join([f'"{c}"' for c in primary_keys])
100
+ columns.append(f"PRIMARY KEY ({pk_columns})")
101
+
102
+ # メタデータカラムを追加
103
+ columns.extend([
104
+ "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP",
105
+ "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
106
+ ])
107
+
108
+ # CREATE TABLE文を構築
109
+ # テーブルが既に存在する場合は作成をスキップ
110
+ if not self._table_exists(db_connection, table_name):
111
+ create_sql = f"CREATE TABLE IF NOT EXISTS {table_name} ({', '.join(columns)})"
112
+
113
+ # テーブルを作成
114
+ try:
115
+ db_connection.execute(create_sql)
116
+ logger.info(f"テーブル '{table_name}' を動的に作成しました")
117
+ except Exception as e:
118
+ # テーブル作成中に他のスレッドが作成した可能性があるため、再度チェック
119
+ if self._table_exists(db_connection, table_name):
120
+ logger.info(f"テーブル '{table_name}' は既に存在します(他のスレッドが作成した可能性)")
121
+ else:
122
+ # それ以外のエラーは再スロー
123
+ raise e
124
+ else:
125
+ logger.info(f"テーブル '{table_name}' は既に存在します")
126
+
127
+
128
+ def _convert_df_to_list(self, df: pd.DataFrame) -> list:
129
+ """
130
+ DataFrameを辞書のリスト形式に変換
131
+
132
+ Args:
133
+ df (pd.DataFrame): 変換するDataFrame
134
+
135
+ Returns:
136
+ list: 各行が辞書形式のリスト
137
+ """
138
+ # 空のDataFrameは空リストを返す
139
+ if df is None or df.empty:
140
+ return []
141
+
142
+ # DataFrameを辞書形式に変換
143
+ # orient='records'で各行を辞書として変換
144
+ return df.to_dict(orient='records')
145
+
146
+
147
+ def __add_db__(self, db_connection: duckdb.DuckDBPyConnection, table_name: str, df: pd.DataFrame, key: str):
148
+
149
+ # カラム整合性チェック
150
+ self._validate_table_schema(db_connection, table_name, df, key)
151
+
152
+ # 既存のkeyのみを取得(パフォーマンス最適化)
153
+ existing_count = db_connection.execute(f"SELECT COUNT(*) as count FROM {table_name}").fetchone()[0]
154
+
155
+ if existing_count == 0:
156
+ # 既存データが空の場合は全データを挿入
157
+ logger.info("既存データが空のため、全データを挿入します")
158
+ self._batch_insert_data(db_connection, table_name, df)
159
+ else:
160
+ # 既存データと新データを比較してユニークな行のみを追加
161
+ if key in df.columns:
162
+ # 既存のkeyのみを効率的に取得
163
+ existing_disclosure_numbers = set(
164
+ db_connection.execute(
165
+ f"SELECT DISTINCT {key} FROM {table_name}"
166
+ ).fetchdf()[key].astype(str)
167
+ )
168
+
169
+ new_disclosure_numbers = set(df[key].astype(str))
170
+
171
+ # 新規のkeyのみを抽出
172
+ unique_disclosure_numbers = new_disclosure_numbers - existing_disclosure_numbers
173
+
174
+ if unique_disclosure_numbers:
175
+ # 新規データのみをフィルタリング
176
+ new_data_df = df[df[key].astype(str).isin(unique_disclosure_numbers)]
177
+
178
+ logger.info(f"新規データ {len(new_data_df)} 件を追加します")
179
+
180
+ # バッチで新規データを挿入
181
+ self._batch_insert_data(db_connection, table_name, new_data_df)
182
+ else:
183
+ logger.info("新規データはありません")
184
+ else:
185
+ # keyがない場合は全データを挿入(重複チェックなし)
186
+ logger.warning(f"{key}カラムが見つからないため、全データを挿入します")
187
+ self._batch_insert_data(db_connection, table_name, df)
188
+
189
+ def __create_db__(self, db_connection: duckdb.DuckDBPyConnection, table_name: str, df: pd.DataFrame, key: str):
190
+ # keyカラムをプライマリキーとして設定(存在する場合)
191
+ primary_keys = [key] if key in df.columns else None
192
+ self._create_table_from_dataframe(db_connection, table_name, df, primary_keys)
193
+
194
+ # インデックスをkeyに作成
195
+ if key in df.columns:
196
+ db_connection.execute(f'CREATE INDEX IF NOT EXISTS idx_{table_name}_{key} ON {table_name}("{key}")')
197
+
198
+ # バッチでデータを挿入
199
+ self._batch_insert_data(db_connection, table_name, df)
200
+
201
+
202
+ def _validate_table_schema(self, db_connection: duckdb.DuckDBPyConnection, table_name: str, df: pd.DataFrame, key: str) -> None:
203
+ """
204
+ 既存テーブルと新データのカラム整合性をチェック
205
+
206
+ Args:
207
+ table_name (str): テーブル名
208
+ df (pd.DataFrame): 新データ
209
+
210
+ Raises:
211
+ ValueError: カラム構造に不整合がある場合
212
+ """
213
+ try:
214
+ # テーブルのカラム情報を取得
215
+ table_info = db_connection.execute(f"PRAGMA table_info({table_name})").fetchdf()
216
+ existing_columns = set(table_info['name'].tolist())
217
+ new_columns = set(df.columns.tolist())
218
+
219
+ # メタデータカラムは自動設定されるため、チェック対象から除外
220
+ metadata_columns = {'created_at', 'updated_at'}
221
+ existing_columns_without_metadata = existing_columns - metadata_columns
222
+
223
+ # カラムの差分をチェック
224
+ missing_columns = existing_columns_without_metadata - new_columns
225
+ extra_columns = new_columns - existing_columns_without_metadata
226
+
227
+ if missing_columns or extra_columns:
228
+ warning_msg = f"カラム構造の不一致を検出しました - テーブル: {table_name}"
229
+ if missing_columns:
230
+ warning_msg += f"\n 新データに不足: {sorted(missing_columns)}"
231
+ if extra_columns:
232
+ warning_msg += f"\n 新データに追加: {sorted(extra_columns)}"
233
+
234
+ logger.warning(warning_msg)
235
+
236
+ # 重要なカラム(key)が不足している場合はエラー
237
+ if key in missing_columns:
238
+ raise ValueError(f"新データに{key}カラムが存在しません")
239
+
240
+ except Exception as e:
241
+ logger.error(f"カラム整合性チェック中にエラーが発生しました: {str(e)}")
242
+ # スキーマチェックでのエラーは警告レベルにとどめ、処理は継続
243
+ if key in str(e):
244
+ raise
245
+
246
+
247
+ def _batch_insert_data(self, db_connection: duckdb.DuckDBPyConnection, table_name: str, df: pd.DataFrame, batch_size: int = 1000) -> None:
248
+ """
249
+ 大量データを効率的にバッチ挿入
250
+
251
+ Args:
252
+ db_connection (duckdb.DuckDBPyConnection): DuckDB接続
253
+ table_name (str): 挿入先テーブル名
254
+ df (pd.DataFrame): 挿入するデータ
255
+ batch_size (int): バッチサイズ(デフォルト: 1000件)
256
+ """
257
+ total_rows = len(df)
258
+
259
+ if total_rows <= batch_size:
260
+ # 小さなデータは一括挿入
261
+ db_connection.register('temp_df', df)
262
+ df_columns = ", ".join(df.columns)
263
+ # DuckDBではINSERT OR REPLACEの代わりにINSERTを使用(重複チェックは呼び出し元で実施)
264
+ db_connection.execute(f"INSERT INTO {table_name} ({df_columns}) SELECT {df_columns} FROM temp_df")
265
+ logger.debug(f"データを一括挿入しました: {total_rows}件")
266
+ else:
267
+ # 大量データはチャンクに分割して挿入
268
+ logger.info(f"大量データをバッチ処理で挿入します: {total_rows}件 (バッチサイズ: {batch_size})")
269
+
270
+ for i in range(0, total_rows, batch_size):
271
+ chunk = df.iloc[i:i+batch_size]
272
+ chunk_name = f'statements_chunk_{i//batch_size}'
273
+
274
+ db_connection.register(chunk_name, chunk)
275
+ df_columns = ", ".join(chunk.columns)
276
+ # DuckDBではINSERT OR REPLACEの代わりにINSERTを使用(重複チェックは呼び出し元で実施)
277
+ db_connection.execute(f"INSERT INTO {table_name} ({df_columns}) SELECT {df_columns} FROM {chunk_name}")
278
+
279
+ # 進捗をログ出力
280
+ processed = min(i + batch_size, total_rows)
281
+ logger.debug(f"バッチ挿入進捗: {processed}/{total_rows} 件 ({processed/total_rows*100:.1f}%)")
282
+
283
+ logger.info(f"バッチ挿入完了: {total_rows}件")