libs.domain.placeholder
libs/domain/placeholder.py
1""" 2libs/domain/placeholder.py 3""" 4 5import logging 6import re 7import textwrap 8from dataclasses import dataclass, field, fields 9from math import ceil 10from typing import TYPE_CHECKING, Any, Literal, Optional, Union 11 12import pandas as pd 13 14from libs.domain.datamodels import ParameterData 15from libs.functions import lookup 16from libs.types import ServiceType 17from libs.utils import dbutil 18from libs.utils.timekit import ExtendedDatetime as ExtDt 19 20if TYPE_CHECKING: 21 from pathlib import Path 22 23 24@dataclass 25class PlaceholderBuilder(ParameterData): 26 """プレースホルダ構築クラス""" 27 28 service_type: ServiceType = field(default=ServiceType.UNKNOWN) 29 """連携先サービス""" 30 command: str = field(default="") 31 """コマンド名""" 32 channel_config: Optional["Path"] = field(default=None) 33 """チャンネル個別設定状況 34 - *Path*: 追加設定ファイルパス 35 - *None*: 個別設定を利用していない 36 """ 37 38 # ルール情報 39 target_mode: int = field(default=0) 40 """集計対象モードの指定 41 - *0*: settingのデフォルトに従う 42 - *not 0*: 指定値でmodeを上書き 43 """ 44 mode: int = field(default=4) 45 """集計モード""" 46 default_rule: str = field(default="") 47 """ルール識別子(設定値)""" 48 rule_version: str = field(default="") 49 """ルール識別子(指定値)""" 50 rule_list: list[str] = field(default_factory=list) 51 """集計対象ルール識別子""" 52 mixed: bool = field(default=False) 53 """ルール識別子の扱い 54 - *True*: 定義済みすべてのルール識別子を含める 55 - *False*: ルール識別子を個別指定 56 """ 57 # ルールセット登録用 58 origin_point: int = field(default=250) 59 """配給原点""" 60 return_point: int = field(default=300) 61 """返し点""" 62 rank_point: str = field(default="") 63 """順位点(空白区切りの文字列)""" 64 ignore_flying: bool = field(default=False) 65 """トビカウントの無効化""" 66 draw_split: bool = field(default=False) 67 """同点時の順位点の取り扱い 68 - *True*: 山分け 69 - *False*: 席順 70 """ 71 undefined_word: int = field(default=1) 72 """未登録ワードの扱い 73 - *0*: 役満扱い 74 - *1*: カウントのみ 75 - *2*: 卓外清算(個人清算) 76 - *3*: 卓外清算(チーム清算) 77 """ 78 79 # 集計対象情報 80 player_name: str = field(default="") 81 """集計対象プレイヤー""" 82 guest_name: str = field(default="") 83 """ゲストの名前""" 84 target_player: list[str] = field(default_factory=list) 85 """引数で受け付けたプレイヤーのリスト""" 86 player_list: list[str] = field(default_factory=list) 87 """集計対象プレイヤーリスト""" 88 competition_list: list[str] = field(default_factory=list) 89 """比較対象プレイヤーリスト""" 90 all_player: bool = field(default=False) 91 """検索対象に登録済みメンバー全員を加える""" 92 source: str = field(default="") 93 """スコア入力元識別子""" 94 separate: bool = field(default=False) 95 """スコア入力元識別子別集計フラグ 96 - *True*: 識別子別に集計 97 - *False*: すべて集計 98 """ 99 collection: str = field(default="") 100 """集約集計 101 - *daily*: 日次集約 102 - *weekly*: 週次集約 103 - *monthly*: 月次集約 104 - *yearly*: 年次集約 105 - *all*: 全体集約 106 """ 107 aggregate_unit: Literal["A", "M", "Y", None] = field(default=None) 108 """レポート生成用日付範囲デフォルト値 109 - *A*: 全期間 110 - *M*: 月別 111 - *Y*: 年別 112 - *None*: 未定義 113 """ 114 target_count: int = field(default=0) 115 """直近ゲーム数指定""" 116 117 starttime: Union[str, ExtDt, None] = field(default=None) 118 """集計開始日時""" 119 endtime: Union[str, ExtDt, None] = field(default=None) 120 """集計終了日時""" 121 onday: Union[str, ExtDt, None] = field(default=None) 122 """time_adjust修正を含まない日時""" 123 124 # 動作/表示変更フラグ 125 score_comparisons: bool = field(default=False) 126 """スコア比較表示""" 127 verbose: bool = field(default=False) 128 """詳細情報表示""" 129 game_results: bool = field(default=False) 130 """ゲーム結果表示""" 131 versus_matrix: bool = field(default=False) 132 """対戦マトリックス表示""" 133 order: bool = field(default=False) 134 """順位推移グラフ表示""" 135 rating: bool = field(default=False) 136 """レーティング推移グラフ表示""" 137 anonymous: bool = field(default=False) 138 """匿名化フラグ""" 139 fourfold: bool = field(default=True) 140 """縦持ち/横持ちデータ判定""" 141 142 # 出力関連 143 guest_mark: str = field(default="※") 144 """ゲスト無効時に未登録メンバーに付与する印""" 145 format: Literal["default", "csv", "txt"] = field(default="default") 146 """出力フォーマット指定""" 147 filename: str = field(default="") 148 """出力ファイル名""" 149 150 # その他 151 database_file: Union[str, "Path"] = field(default="") 152 """成績管理データベースファイル名""" 153 logging_verbose: int = field(default=0) 154 """デバッグ情報出力レベル""" 155 156 def update_from_dict(self, input_dict: dict[str, Any]) -> None: 157 """ 158 辞書の内容で値を更新する 159 160 Args: 161 input_dict (dict[str,Any]): 更新内容 162 163 """ 164 field_list: list[str] = [x.name for x in fields(self)] 165 for k, v in input_dict.items(): 166 if k in field_list: 167 setattr(self, k, v) 168 169 def update_setting(self, main_config: "Path", key_name: str, val_type: type, fallback: Any = None) -> None: 170 """ 171 優先度順に key_name を探索して値を更新する 172 173 Args: 174 main_config (Path): メイン設定ファイルパス 175 key_name (str): 探索するキー名 176 val_type (type): 取り込む値の型 (bool, str) 177 fallback (Any): 見つからなかった場合にセットする値 178 - *None* が指定されているときは値を更新しない 179 180 Note: 181 探索優先順序 182 1. 個別設定ファイル内settingセクション 183 2. メイン設定ファイル内チャンネル個別セクション 184 3. メイン設定ファイル内サービス別セクション 185 4. メイン設定ファイル内settingセクション 186 187 """ 188 value: Optional[Any] = None 189 # 個別設定ファイル内探索 190 if self.channel_config: 191 value = lookup.get_config_value( 192 config_file=self.channel_config, 193 section="setting", 194 name=key_name, 195 val_type=val_type, 196 fallback=None, 197 ) 198 if value is not None: 199 setattr(self, key_name, value) 200 return 201 202 # メイン設定ファイル内探索 203 for section_name in [self.source, self.service_type, "setting"]: 204 value = lookup.get_config_value( 205 config_file=main_config, 206 section=section_name, 207 name=key_name, 208 val_type=val_type, 209 fallback=None, 210 ) 211 if value is not None: 212 setattr(self, key_name, value) 213 return 214 215 if fallback is not None: 216 setattr(self, key_name, fallback) 217 218 def query_modification(self, query: str) -> str: 219 """ 220 クエリをオプションの内容で修正する 221 222 Args: 223 query (str): 修正するクエリ 224 225 Returns: 226 str: 修正後のクエリ 227 228 """ 229 if self.individual: # 個人集計 230 query = query.replace("--[individual] ", "") 231 # ゲスト関連フラグ 232 if self.unregistered_replace: 233 query = query.replace("--[unregistered_replace] ", "") 234 if self.guest_skip: 235 query = query.replace("--[guest_not_skip] ", "") 236 else: 237 query = query.replace("--[guest_skip] ", "") 238 else: 239 query = query.replace("--[unregistered_not_replace] ", "") 240 else: # チーム集計 241 self.unregistered_replace = False 242 self.guest_skip = True 243 query = query.replace("--[team] ", "") 244 if not self.friendly_fire: 245 query = query.replace("--[friendly_fire] ", "") 246 247 # 集約集計 248 match self.collection: 249 case "daily": 250 query = query.replace("--[collection_daily] ", "") 251 query = query.replace("--[collection] ", "") 252 case "weekly": 253 query = query.replace("--[collection_weekly] ", "") 254 query = query.replace("--[collection] ", "") 255 case "monthly": 256 query = query.replace("--[collection_monthly] ", "") 257 query = query.replace("--[collection] ", "") 258 case "yearly": 259 query = query.replace("--[collection_yearly] ", "") 260 query = query.replace("--[collection] ", "") 261 case "all": 262 query = query.replace("--[collection_all] ", "") 263 query = query.replace("--[collection] ", "") 264 case _: 265 query = query.replace("--[not_collection] ", "") 266 267 # 集計対象ルール 268 if self.rule_list: 269 query = query.replace("<<rule_list>>", ",".join([f":rule_{idx}" for idx, _ in enumerate(self.rule_list)])) 270 else: 271 query = query.replace("and rule_version in (<<rule_list>>)", "") 272 query = query.replace("and results.rule_version in (<<rule_list>>)", "") 273 query = query.replace("and game_info.rule_version in (<<rule_list>>)", "") 274 275 # 集計モード 276 match self.mode: 277 case 3: 278 query = query.replace("--[mode3] ", "") 279 case 4: 280 query = query.replace("--[mode4] ", "") 281 282 # スコア入力元識別子別集計 283 if self.separate: 284 query = query.replace("--[separate] ", "") 285 286 # コメント検索 287 if self.search_word or self.group_length: 288 query = query.replace("--[group_by] ", "") 289 else: 290 query = query.replace("--[not_group_by] ", "") 291 292 if self.search_word: 293 query = query.replace("--[search_word] ", "") 294 else: 295 query = query.replace("--[not_search_word] ", "") 296 297 if self.group_length: 298 query = query.replace("--[group_length] ", "") 299 else: 300 query = query.replace("--[not_group_length] ", "") 301 if self.search_word: 302 query = query.replace("--[comment] ", "") 303 else: 304 query = query.replace("--[not_comment] ", "") 305 306 # 直近N検索用(全範囲取得してから絞る) 307 if self.target_count: 308 query = query.replace("and my.playtime between", "-- and my.playtime between") 309 310 # プレイヤーリスト 311 if self.player_name and self.player_list: 312 query = query.replace("--[player_name] ", "") 313 query = query.replace( 314 "<<player_list>>", 315 ", ".join([f":player_{idx}" for idx, _ in enumerate(self.player_list)]), 316 ) 317 query = query.replace("<<guest_mark>>", self.guest_mark) 318 319 # フラグの処理 320 match self.aggregate_unit: 321 case "M": 322 query = query.replace("<<collection>>", "substr(collection_daily, 1, 7) as 集計") 323 query = query.replace("<<group by>>", "group by 集計") 324 case "Y": 325 query = query.replace("<<collection>>", "substr(collection_daily, 1, 4) as 集計") 326 query = query.replace("<<group by>>", "group by 集計") 327 case "A": 328 query = query.replace("<<collection>>", "'合計' as 集計") 329 query = query.replace("<<group by>>", "") 330 case _: 331 query = query.replace("<<collection>>,", "-- <<collection>>") 332 query = query.replace("<<group by>>", "-- <<group by>>") 333 334 if self.interval: 335 query = query.replace("<<Calculation Formula>>", "(row_number() over (order by total_count desc) - 1) / :interval") 336 else: 337 query = query.replace("<<Calculation Formula>>", ":interval") 338 339 if self.undefined_word is not None: 340 match self.undefined_word: 341 case 0: 342 query = query.replace("<<where_string>>", "and (words.type is null or words.type = 0)") 343 case 1: 344 query = query.replace("<<where_string>>", "and (words.type is null or words.type = 1)") 345 case 2: 346 query = query.replace("<<where_string>>", "and (words.type is null or words.type = 2)") 347 case _: 348 query = query.replace("<<where_string>>", "and (words.type = 1 or words.type = 2)") 349 else: 350 query = query.replace(":undefined_word", "1") 351 352 # queryコメント削除 353 query = re.sub(r"^ *--\[.*$", "", query, flags=re.MULTILINE) 354 query = re.sub(r"\n+", "\n", query, flags=re.MULTILINE) 355 356 return query 357 358 def named_query(self, query: str) -> str: 359 """ 360 クエリにパラメータをバインドして返す 361 362 Args: 363 query (str): SQL 364 365 Returns: 366 str: バインド済みSQL 367 368 """ 369 return textwrap.dedent( 370 re.sub( 371 r":(\w+)", 372 lambda m: repr(self.placeholder().get(m.group(1), m.group(0))), 373 query, 374 ), 375 ).strip() 376 377 def placeholder(self, game_count: Optional[int] = None) -> dict[str, Any]: 378 """ 379 プレースホルダ用辞書出力 380 381 Args: 382 game_count (Optional[int]): 規定打数調整用ゲーム数. Defaults to None. 383 384 Returns: 385 dict[str, Any]: プレースホルダ 386 387 """ 388 ret_dict: dict[str, Any] = {f.name: getattr(self, f.name) for f in fields(self)} 389 390 # 規定打数更新 391 if not ret_dict.get("stipulated") or game_count is not None: 392 if game_count is None: 393 ret_dict.update({"stipulated": 1}) 394 else: 395 ret_dict.update({"stipulated": int(ceil(game_count * self.stipulated_rate) + 1)}) 396 397 if self.player_list: 398 ret_dict.update({f"player_{idx}": x for idx, x in enumerate(self.player_list)}) 399 400 if self.target_player: 401 ret_dict.update({f"target_{idx}": x for idx, x in enumerate(self.target_player)}) 402 403 if self.competition_list: 404 ret_dict.update({f"competition_{idx}": x for idx, x in enumerate(self.competition_list)}) 405 406 if self.rule_list: 407 ret_dict.update({f"rule_{idx}": x for idx, x in enumerate(self.rule_list)}) 408 else: 409 ret_dict.update({"rule_0": self.rule_version}) 410 411 # 日付型変換 412 for date_attr in ["starttime", "endtime", "onday"]: 413 if (val := ret_dict.get(date_attr)) and isinstance(val, ExtDt): 414 ret_dict.update({date_attr: val.format(ExtDt.FMT.SQL)}) 415 416 return ret_dict 417 418 def read_data(self, keyword: str) -> pd.DataFrame: 419 """ 420 データベースからデータを取得する 421 422 Args: 423 keyword (str): SQL選択キーワード 424 425 Returns: 426 pd.DataFrame: 集計結果 427 428 """ 429 query = self.query_modification(dbutil.query(keyword)) 430 431 if self.logging_verbose & 0x01: 432 print(f">>> params={self.placeholder()}") 433 print(f">>> SQL: {keyword} -> {self.database_file}\n{self.named_query(query)}") 434 435 try: 436 query_start_time = ExtDt().dt.timestamp() 437 df = pd.read_sql( 438 sql=query, 439 con=dbutil.connection(self.database_file), 440 params=self.placeholder(), 441 ) 442 query_end_time = ExtDt().dt.timestamp() 443 except pd.errors.DatabaseError as err: 444 logging.error("DatabaseError: %s", err) 445 logging.error("SQL: %s, DATABASE: %s", keyword, self.database_file) 446 logging.error("params=%s", self.placeholder()) 447 logging.error("query: %s", self.named_query(query)) 448 449 if self.logging_verbose & 0x02: 450 print("=" * 80) 451 print(df.to_string()) 452 453 logging.debug("SQL: %s, time: %s", keyword, query_end_time - query_start_time) 454 return df
25@dataclass 26class PlaceholderBuilder(ParameterData): 27 """プレースホルダ構築クラス""" 28 29 service_type: ServiceType = field(default=ServiceType.UNKNOWN) 30 """連携先サービス""" 31 command: str = field(default="") 32 """コマンド名""" 33 channel_config: Optional["Path"] = field(default=None) 34 """チャンネル個別設定状況 35 - *Path*: 追加設定ファイルパス 36 - *None*: 個別設定を利用していない 37 """ 38 39 # ルール情報 40 target_mode: int = field(default=0) 41 """集計対象モードの指定 42 - *0*: settingのデフォルトに従う 43 - *not 0*: 指定値でmodeを上書き 44 """ 45 mode: int = field(default=4) 46 """集計モード""" 47 default_rule: str = field(default="") 48 """ルール識別子(設定値)""" 49 rule_version: str = field(default="") 50 """ルール識別子(指定値)""" 51 rule_list: list[str] = field(default_factory=list) 52 """集計対象ルール識別子""" 53 mixed: bool = field(default=False) 54 """ルール識別子の扱い 55 - *True*: 定義済みすべてのルール識別子を含める 56 - *False*: ルール識別子を個別指定 57 """ 58 # ルールセット登録用 59 origin_point: int = field(default=250) 60 """配給原点""" 61 return_point: int = field(default=300) 62 """返し点""" 63 rank_point: str = field(default="") 64 """順位点(空白区切りの文字列)""" 65 ignore_flying: bool = field(default=False) 66 """トビカウントの無効化""" 67 draw_split: bool = field(default=False) 68 """同点時の順位点の取り扱い 69 - *True*: 山分け 70 - *False*: 席順 71 """ 72 undefined_word: int = field(default=1) 73 """未登録ワードの扱い 74 - *0*: 役満扱い 75 - *1*: カウントのみ 76 - *2*: 卓外清算(個人清算) 77 - *3*: 卓外清算(チーム清算) 78 """ 79 80 # 集計対象情報 81 player_name: str = field(default="") 82 """集計対象プレイヤー""" 83 guest_name: str = field(default="") 84 """ゲストの名前""" 85 target_player: list[str] = field(default_factory=list) 86 """引数で受け付けたプレイヤーのリスト""" 87 player_list: list[str] = field(default_factory=list) 88 """集計対象プレイヤーリスト""" 89 competition_list: list[str] = field(default_factory=list) 90 """比較対象プレイヤーリスト""" 91 all_player: bool = field(default=False) 92 """検索対象に登録済みメンバー全員を加える""" 93 source: str = field(default="") 94 """スコア入力元識別子""" 95 separate: bool = field(default=False) 96 """スコア入力元識別子別集計フラグ 97 - *True*: 識別子別に集計 98 - *False*: すべて集計 99 """ 100 collection: str = field(default="") 101 """集約集計 102 - *daily*: 日次集約 103 - *weekly*: 週次集約 104 - *monthly*: 月次集約 105 - *yearly*: 年次集約 106 - *all*: 全体集約 107 """ 108 aggregate_unit: Literal["A", "M", "Y", None] = field(default=None) 109 """レポート生成用日付範囲デフォルト値 110 - *A*: 全期間 111 - *M*: 月別 112 - *Y*: 年別 113 - *None*: 未定義 114 """ 115 target_count: int = field(default=0) 116 """直近ゲーム数指定""" 117 118 starttime: Union[str, ExtDt, None] = field(default=None) 119 """集計開始日時""" 120 endtime: Union[str, ExtDt, None] = field(default=None) 121 """集計終了日時""" 122 onday: Union[str, ExtDt, None] = field(default=None) 123 """time_adjust修正を含まない日時""" 124 125 # 動作/表示変更フラグ 126 score_comparisons: bool = field(default=False) 127 """スコア比較表示""" 128 verbose: bool = field(default=False) 129 """詳細情報表示""" 130 game_results: bool = field(default=False) 131 """ゲーム結果表示""" 132 versus_matrix: bool = field(default=False) 133 """対戦マトリックス表示""" 134 order: bool = field(default=False) 135 """順位推移グラフ表示""" 136 rating: bool = field(default=False) 137 """レーティング推移グラフ表示""" 138 anonymous: bool = field(default=False) 139 """匿名化フラグ""" 140 fourfold: bool = field(default=True) 141 """縦持ち/横持ちデータ判定""" 142 143 # 出力関連 144 guest_mark: str = field(default="※") 145 """ゲスト無効時に未登録メンバーに付与する印""" 146 format: Literal["default", "csv", "txt"] = field(default="default") 147 """出力フォーマット指定""" 148 filename: str = field(default="") 149 """出力ファイル名""" 150 151 # その他 152 database_file: Union[str, "Path"] = field(default="") 153 """成績管理データベースファイル名""" 154 logging_verbose: int = field(default=0) 155 """デバッグ情報出力レベル""" 156 157 def update_from_dict(self, input_dict: dict[str, Any]) -> None: 158 """ 159 辞書の内容で値を更新する 160 161 Args: 162 input_dict (dict[str,Any]): 更新内容 163 164 """ 165 field_list: list[str] = [x.name for x in fields(self)] 166 for k, v in input_dict.items(): 167 if k in field_list: 168 setattr(self, k, v) 169 170 def update_setting(self, main_config: "Path", key_name: str, val_type: type, fallback: Any = None) -> None: 171 """ 172 優先度順に key_name を探索して値を更新する 173 174 Args: 175 main_config (Path): メイン設定ファイルパス 176 key_name (str): 探索するキー名 177 val_type (type): 取り込む値の型 (bool, str) 178 fallback (Any): 見つからなかった場合にセットする値 179 - *None* が指定されているときは値を更新しない 180 181 Note: 182 探索優先順序 183 1. 個別設定ファイル内settingセクション 184 2. メイン設定ファイル内チャンネル個別セクション 185 3. メイン設定ファイル内サービス別セクション 186 4. メイン設定ファイル内settingセクション 187 188 """ 189 value: Optional[Any] = None 190 # 個別設定ファイル内探索 191 if self.channel_config: 192 value = lookup.get_config_value( 193 config_file=self.channel_config, 194 section="setting", 195 name=key_name, 196 val_type=val_type, 197 fallback=None, 198 ) 199 if value is not None: 200 setattr(self, key_name, value) 201 return 202 203 # メイン設定ファイル内探索 204 for section_name in [self.source, self.service_type, "setting"]: 205 value = lookup.get_config_value( 206 config_file=main_config, 207 section=section_name, 208 name=key_name, 209 val_type=val_type, 210 fallback=None, 211 ) 212 if value is not None: 213 setattr(self, key_name, value) 214 return 215 216 if fallback is not None: 217 setattr(self, key_name, fallback) 218 219 def query_modification(self, query: str) -> str: 220 """ 221 クエリをオプションの内容で修正する 222 223 Args: 224 query (str): 修正するクエリ 225 226 Returns: 227 str: 修正後のクエリ 228 229 """ 230 if self.individual: # 個人集計 231 query = query.replace("--[individual] ", "") 232 # ゲスト関連フラグ 233 if self.unregistered_replace: 234 query = query.replace("--[unregistered_replace] ", "") 235 if self.guest_skip: 236 query = query.replace("--[guest_not_skip] ", "") 237 else: 238 query = query.replace("--[guest_skip] ", "") 239 else: 240 query = query.replace("--[unregistered_not_replace] ", "") 241 else: # チーム集計 242 self.unregistered_replace = False 243 self.guest_skip = True 244 query = query.replace("--[team] ", "") 245 if not self.friendly_fire: 246 query = query.replace("--[friendly_fire] ", "") 247 248 # 集約集計 249 match self.collection: 250 case "daily": 251 query = query.replace("--[collection_daily] ", "") 252 query = query.replace("--[collection] ", "") 253 case "weekly": 254 query = query.replace("--[collection_weekly] ", "") 255 query = query.replace("--[collection] ", "") 256 case "monthly": 257 query = query.replace("--[collection_monthly] ", "") 258 query = query.replace("--[collection] ", "") 259 case "yearly": 260 query = query.replace("--[collection_yearly] ", "") 261 query = query.replace("--[collection] ", "") 262 case "all": 263 query = query.replace("--[collection_all] ", "") 264 query = query.replace("--[collection] ", "") 265 case _: 266 query = query.replace("--[not_collection] ", "") 267 268 # 集計対象ルール 269 if self.rule_list: 270 query = query.replace("<<rule_list>>", ",".join([f":rule_{idx}" for idx, _ in enumerate(self.rule_list)])) 271 else: 272 query = query.replace("and rule_version in (<<rule_list>>)", "") 273 query = query.replace("and results.rule_version in (<<rule_list>>)", "") 274 query = query.replace("and game_info.rule_version in (<<rule_list>>)", "") 275 276 # 集計モード 277 match self.mode: 278 case 3: 279 query = query.replace("--[mode3] ", "") 280 case 4: 281 query = query.replace("--[mode4] ", "") 282 283 # スコア入力元識別子別集計 284 if self.separate: 285 query = query.replace("--[separate] ", "") 286 287 # コメント検索 288 if self.search_word or self.group_length: 289 query = query.replace("--[group_by] ", "") 290 else: 291 query = query.replace("--[not_group_by] ", "") 292 293 if self.search_word: 294 query = query.replace("--[search_word] ", "") 295 else: 296 query = query.replace("--[not_search_word] ", "") 297 298 if self.group_length: 299 query = query.replace("--[group_length] ", "") 300 else: 301 query = query.replace("--[not_group_length] ", "") 302 if self.search_word: 303 query = query.replace("--[comment] ", "") 304 else: 305 query = query.replace("--[not_comment] ", "") 306 307 # 直近N検索用(全範囲取得してから絞る) 308 if self.target_count: 309 query = query.replace("and my.playtime between", "-- and my.playtime between") 310 311 # プレイヤーリスト 312 if self.player_name and self.player_list: 313 query = query.replace("--[player_name] ", "") 314 query = query.replace( 315 "<<player_list>>", 316 ", ".join([f":player_{idx}" for idx, _ in enumerate(self.player_list)]), 317 ) 318 query = query.replace("<<guest_mark>>", self.guest_mark) 319 320 # フラグの処理 321 match self.aggregate_unit: 322 case "M": 323 query = query.replace("<<collection>>", "substr(collection_daily, 1, 7) as 集計") 324 query = query.replace("<<group by>>", "group by 集計") 325 case "Y": 326 query = query.replace("<<collection>>", "substr(collection_daily, 1, 4) as 集計") 327 query = query.replace("<<group by>>", "group by 集計") 328 case "A": 329 query = query.replace("<<collection>>", "'合計' as 集計") 330 query = query.replace("<<group by>>", "") 331 case _: 332 query = query.replace("<<collection>>,", "-- <<collection>>") 333 query = query.replace("<<group by>>", "-- <<group by>>") 334 335 if self.interval: 336 query = query.replace("<<Calculation Formula>>", "(row_number() over (order by total_count desc) - 1) / :interval") 337 else: 338 query = query.replace("<<Calculation Formula>>", ":interval") 339 340 if self.undefined_word is not None: 341 match self.undefined_word: 342 case 0: 343 query = query.replace("<<where_string>>", "and (words.type is null or words.type = 0)") 344 case 1: 345 query = query.replace("<<where_string>>", "and (words.type is null or words.type = 1)") 346 case 2: 347 query = query.replace("<<where_string>>", "and (words.type is null or words.type = 2)") 348 case _: 349 query = query.replace("<<where_string>>", "and (words.type = 1 or words.type = 2)") 350 else: 351 query = query.replace(":undefined_word", "1") 352 353 # queryコメント削除 354 query = re.sub(r"^ *--\[.*$", "", query, flags=re.MULTILINE) 355 query = re.sub(r"\n+", "\n", query, flags=re.MULTILINE) 356 357 return query 358 359 def named_query(self, query: str) -> str: 360 """ 361 クエリにパラメータをバインドして返す 362 363 Args: 364 query (str): SQL 365 366 Returns: 367 str: バインド済みSQL 368 369 """ 370 return textwrap.dedent( 371 re.sub( 372 r":(\w+)", 373 lambda m: repr(self.placeholder().get(m.group(1), m.group(0))), 374 query, 375 ), 376 ).strip() 377 378 def placeholder(self, game_count: Optional[int] = None) -> dict[str, Any]: 379 """ 380 プレースホルダ用辞書出力 381 382 Args: 383 game_count (Optional[int]): 規定打数調整用ゲーム数. Defaults to None. 384 385 Returns: 386 dict[str, Any]: プレースホルダ 387 388 """ 389 ret_dict: dict[str, Any] = {f.name: getattr(self, f.name) for f in fields(self)} 390 391 # 規定打数更新 392 if not ret_dict.get("stipulated") or game_count is not None: 393 if game_count is None: 394 ret_dict.update({"stipulated": 1}) 395 else: 396 ret_dict.update({"stipulated": int(ceil(game_count * self.stipulated_rate) + 1)}) 397 398 if self.player_list: 399 ret_dict.update({f"player_{idx}": x for idx, x in enumerate(self.player_list)}) 400 401 if self.target_player: 402 ret_dict.update({f"target_{idx}": x for idx, x in enumerate(self.target_player)}) 403 404 if self.competition_list: 405 ret_dict.update({f"competition_{idx}": x for idx, x in enumerate(self.competition_list)}) 406 407 if self.rule_list: 408 ret_dict.update({f"rule_{idx}": x for idx, x in enumerate(self.rule_list)}) 409 else: 410 ret_dict.update({"rule_0": self.rule_version}) 411 412 # 日付型変換 413 for date_attr in ["starttime", "endtime", "onday"]: 414 if (val := ret_dict.get(date_attr)) and isinstance(val, ExtDt): 415 ret_dict.update({date_attr: val.format(ExtDt.FMT.SQL)}) 416 417 return ret_dict 418 419 def read_data(self, keyword: str) -> pd.DataFrame: 420 """ 421 データベースからデータを取得する 422 423 Args: 424 keyword (str): SQL選択キーワード 425 426 Returns: 427 pd.DataFrame: 集計結果 428 429 """ 430 query = self.query_modification(dbutil.query(keyword)) 431 432 if self.logging_verbose & 0x01: 433 print(f">>> params={self.placeholder()}") 434 print(f">>> SQL: {keyword} -> {self.database_file}\n{self.named_query(query)}") 435 436 try: 437 query_start_time = ExtDt().dt.timestamp() 438 df = pd.read_sql( 439 sql=query, 440 con=dbutil.connection(self.database_file), 441 params=self.placeholder(), 442 ) 443 query_end_time = ExtDt().dt.timestamp() 444 except pd.errors.DatabaseError as err: 445 logging.error("DatabaseError: %s", err) 446 logging.error("SQL: %s, DATABASE: %s", keyword, self.database_file) 447 logging.error("params=%s", self.placeholder()) 448 logging.error("query: %s", self.named_query(query)) 449 450 if self.logging_verbose & 0x02: 451 print("=" * 80) 452 print(df.to_string()) 453 454 logging.debug("SQL: %s, time: %s", keyword, query_end_time - query_start_time) 455 return df
プレースホルダ構築クラス
PlaceholderBuilder( individual: bool = True, guest_skip: bool = True, guest_skip2: bool = True, unregistered_replace: bool = True, friendly_fire: bool = False, statistics: bool = False, ranked: int = 3, stipulated: int = 0, stipulated_rate: float = 0.05, interval: int = 80, search_word: str = '', group_length: int = 0, service_type: libs.types.ServiceType = <ServiceType.UNKNOWN: 'unknown'>, command: str = '', channel_config: pathlib.Path | None = None, target_mode: int = 0, mode: int = 4, default_rule: str = '', rule_version: str = '', rule_list: list[str] = <factory>, mixed: bool = False, origin_point: int = 250, return_point: int = 300, rank_point: str = '', ignore_flying: bool = False, draw_split: bool = False, undefined_word: int = 1, player_name: str = '', guest_name: str = '', target_player: list[str] = <factory>, player_list: list[str] = <factory>, competition_list: list[str] = <factory>, all_player: bool = False, source: str = '', separate: bool = False, collection: str = '', aggregate_unit: Literal['A', 'M', 'Y', None] = None, target_count: int = 0, starttime: str | libs.utils.timekit.ExtendedDatetime | None = None, endtime: str | libs.utils.timekit.ExtendedDatetime | None = None, onday: str | libs.utils.timekit.ExtendedDatetime | None = None, score_comparisons: bool = False, verbose: bool = False, game_results: bool = False, versus_matrix: bool = False, order: bool = False, rating: bool = False, anonymous: bool = False, fourfold: bool = True, guest_mark: str = '※', format: Literal['default', 'csv', 'txt'] = 'default', filename: str = '', database_file: str | pathlib.Path = '', logging_verbose: int = 0)
def
update_from_dict(self, input_dict: dict[str, typing.Any]) -> None:
157 def update_from_dict(self, input_dict: dict[str, Any]) -> None: 158 """ 159 辞書の内容で値を更新する 160 161 Args: 162 input_dict (dict[str,Any]): 更新内容 163 164 """ 165 field_list: list[str] = [x.name for x in fields(self)] 166 for k, v in input_dict.items(): 167 if k in field_list: 168 setattr(self, k, v)
辞書の内容で値を更新する
Arguments:
- input_dict (dict[str,Any]): 更新内容
def
update_setting( self, main_config: pathlib.Path, key_name: str, val_type: type, fallback: Any = None) -> None:
170 def update_setting(self, main_config: "Path", key_name: str, val_type: type, fallback: Any = None) -> None: 171 """ 172 優先度順に key_name を探索して値を更新する 173 174 Args: 175 main_config (Path): メイン設定ファイルパス 176 key_name (str): 探索するキー名 177 val_type (type): 取り込む値の型 (bool, str) 178 fallback (Any): 見つからなかった場合にセットする値 179 - *None* が指定されているときは値を更新しない 180 181 Note: 182 探索優先順序 183 1. 個別設定ファイル内settingセクション 184 2. メイン設定ファイル内チャンネル個別セクション 185 3. メイン設定ファイル内サービス別セクション 186 4. メイン設定ファイル内settingセクション 187 188 """ 189 value: Optional[Any] = None 190 # 個別設定ファイル内探索 191 if self.channel_config: 192 value = lookup.get_config_value( 193 config_file=self.channel_config, 194 section="setting", 195 name=key_name, 196 val_type=val_type, 197 fallback=None, 198 ) 199 if value is not None: 200 setattr(self, key_name, value) 201 return 202 203 # メイン設定ファイル内探索 204 for section_name in [self.source, self.service_type, "setting"]: 205 value = lookup.get_config_value( 206 config_file=main_config, 207 section=section_name, 208 name=key_name, 209 val_type=val_type, 210 fallback=None, 211 ) 212 if value is not None: 213 setattr(self, key_name, value) 214 return 215 216 if fallback is not None: 217 setattr(self, key_name, fallback)
優先度順に key_name を探索して値を更新する
Arguments:
- main_config (Path): メイン設定ファイルパス
- key_name (str): 探索するキー名
- val_type (type): 取り込む値の型 (bool, str)
- fallback (Any): 見つからなかった場合にセットする値
- None が指定されているときは値を更新しない
Note:
探索優先順序
- 個別設定ファイル内settingセクション
- メイン設定ファイル内チャンネル個別セクション
- メイン設定ファイル内サービス別セクション
- メイン設定ファイル内settingセクション
def
query_modification(self, query: str) -> str:
219 def query_modification(self, query: str) -> str: 220 """ 221 クエリをオプションの内容で修正する 222 223 Args: 224 query (str): 修正するクエリ 225 226 Returns: 227 str: 修正後のクエリ 228 229 """ 230 if self.individual: # 個人集計 231 query = query.replace("--[individual] ", "") 232 # ゲスト関連フラグ 233 if self.unregistered_replace: 234 query = query.replace("--[unregistered_replace] ", "") 235 if self.guest_skip: 236 query = query.replace("--[guest_not_skip] ", "") 237 else: 238 query = query.replace("--[guest_skip] ", "") 239 else: 240 query = query.replace("--[unregistered_not_replace] ", "") 241 else: # チーム集計 242 self.unregistered_replace = False 243 self.guest_skip = True 244 query = query.replace("--[team] ", "") 245 if not self.friendly_fire: 246 query = query.replace("--[friendly_fire] ", "") 247 248 # 集約集計 249 match self.collection: 250 case "daily": 251 query = query.replace("--[collection_daily] ", "") 252 query = query.replace("--[collection] ", "") 253 case "weekly": 254 query = query.replace("--[collection_weekly] ", "") 255 query = query.replace("--[collection] ", "") 256 case "monthly": 257 query = query.replace("--[collection_monthly] ", "") 258 query = query.replace("--[collection] ", "") 259 case "yearly": 260 query = query.replace("--[collection_yearly] ", "") 261 query = query.replace("--[collection] ", "") 262 case "all": 263 query = query.replace("--[collection_all] ", "") 264 query = query.replace("--[collection] ", "") 265 case _: 266 query = query.replace("--[not_collection] ", "") 267 268 # 集計対象ルール 269 if self.rule_list: 270 query = query.replace("<<rule_list>>", ",".join([f":rule_{idx}" for idx, _ in enumerate(self.rule_list)])) 271 else: 272 query = query.replace("and rule_version in (<<rule_list>>)", "") 273 query = query.replace("and results.rule_version in (<<rule_list>>)", "") 274 query = query.replace("and game_info.rule_version in (<<rule_list>>)", "") 275 276 # 集計モード 277 match self.mode: 278 case 3: 279 query = query.replace("--[mode3] ", "") 280 case 4: 281 query = query.replace("--[mode4] ", "") 282 283 # スコア入力元識別子別集計 284 if self.separate: 285 query = query.replace("--[separate] ", "") 286 287 # コメント検索 288 if self.search_word or self.group_length: 289 query = query.replace("--[group_by] ", "") 290 else: 291 query = query.replace("--[not_group_by] ", "") 292 293 if self.search_word: 294 query = query.replace("--[search_word] ", "") 295 else: 296 query = query.replace("--[not_search_word] ", "") 297 298 if self.group_length: 299 query = query.replace("--[group_length] ", "") 300 else: 301 query = query.replace("--[not_group_length] ", "") 302 if self.search_word: 303 query = query.replace("--[comment] ", "") 304 else: 305 query = query.replace("--[not_comment] ", "") 306 307 # 直近N検索用(全範囲取得してから絞る) 308 if self.target_count: 309 query = query.replace("and my.playtime between", "-- and my.playtime between") 310 311 # プレイヤーリスト 312 if self.player_name and self.player_list: 313 query = query.replace("--[player_name] ", "") 314 query = query.replace( 315 "<<player_list>>", 316 ", ".join([f":player_{idx}" for idx, _ in enumerate(self.player_list)]), 317 ) 318 query = query.replace("<<guest_mark>>", self.guest_mark) 319 320 # フラグの処理 321 match self.aggregate_unit: 322 case "M": 323 query = query.replace("<<collection>>", "substr(collection_daily, 1, 7) as 集計") 324 query = query.replace("<<group by>>", "group by 集計") 325 case "Y": 326 query = query.replace("<<collection>>", "substr(collection_daily, 1, 4) as 集計") 327 query = query.replace("<<group by>>", "group by 集計") 328 case "A": 329 query = query.replace("<<collection>>", "'合計' as 集計") 330 query = query.replace("<<group by>>", "") 331 case _: 332 query = query.replace("<<collection>>,", "-- <<collection>>") 333 query = query.replace("<<group by>>", "-- <<group by>>") 334 335 if self.interval: 336 query = query.replace("<<Calculation Formula>>", "(row_number() over (order by total_count desc) - 1) / :interval") 337 else: 338 query = query.replace("<<Calculation Formula>>", ":interval") 339 340 if self.undefined_word is not None: 341 match self.undefined_word: 342 case 0: 343 query = query.replace("<<where_string>>", "and (words.type is null or words.type = 0)") 344 case 1: 345 query = query.replace("<<where_string>>", "and (words.type is null or words.type = 1)") 346 case 2: 347 query = query.replace("<<where_string>>", "and (words.type is null or words.type = 2)") 348 case _: 349 query = query.replace("<<where_string>>", "and (words.type = 1 or words.type = 2)") 350 else: 351 query = query.replace(":undefined_word", "1") 352 353 # queryコメント削除 354 query = re.sub(r"^ *--\[.*$", "", query, flags=re.MULTILINE) 355 query = re.sub(r"\n+", "\n", query, flags=re.MULTILINE) 356 357 return query
クエリをオプションの内容で修正する
Arguments:
- query (str): 修正するクエリ
Returns:
str: 修正後のクエリ
def
named_query(self, query: str) -> str:
359 def named_query(self, query: str) -> str: 360 """ 361 クエリにパラメータをバインドして返す 362 363 Args: 364 query (str): SQL 365 366 Returns: 367 str: バインド済みSQL 368 369 """ 370 return textwrap.dedent( 371 re.sub( 372 r":(\w+)", 373 lambda m: repr(self.placeholder().get(m.group(1), m.group(0))), 374 query, 375 ), 376 ).strip()
クエリにパラメータをバインドして返す
Arguments:
- query (str): SQL
Returns:
str: バインド済みSQL
def
placeholder(self, game_count: int | None = None) -> dict[str, typing.Any]:
378 def placeholder(self, game_count: Optional[int] = None) -> dict[str, Any]: 379 """ 380 プレースホルダ用辞書出力 381 382 Args: 383 game_count (Optional[int]): 規定打数調整用ゲーム数. Defaults to None. 384 385 Returns: 386 dict[str, Any]: プレースホルダ 387 388 """ 389 ret_dict: dict[str, Any] = {f.name: getattr(self, f.name) for f in fields(self)} 390 391 # 規定打数更新 392 if not ret_dict.get("stipulated") or game_count is not None: 393 if game_count is None: 394 ret_dict.update({"stipulated": 1}) 395 else: 396 ret_dict.update({"stipulated": int(ceil(game_count * self.stipulated_rate) + 1)}) 397 398 if self.player_list: 399 ret_dict.update({f"player_{idx}": x for idx, x in enumerate(self.player_list)}) 400 401 if self.target_player: 402 ret_dict.update({f"target_{idx}": x for idx, x in enumerate(self.target_player)}) 403 404 if self.competition_list: 405 ret_dict.update({f"competition_{idx}": x for idx, x in enumerate(self.competition_list)}) 406 407 if self.rule_list: 408 ret_dict.update({f"rule_{idx}": x for idx, x in enumerate(self.rule_list)}) 409 else: 410 ret_dict.update({"rule_0": self.rule_version}) 411 412 # 日付型変換 413 for date_attr in ["starttime", "endtime", "onday"]: 414 if (val := ret_dict.get(date_attr)) and isinstance(val, ExtDt): 415 ret_dict.update({date_attr: val.format(ExtDt.FMT.SQL)}) 416 417 return ret_dict
プレースホルダ用辞書出力
Arguments:
- game_count (Optional[int]): 規定打数調整用ゲーム数. Defaults to None.
Returns:
dict[str, Any]: プレースホルダ
def
read_data(self, keyword: str) -> pandas.DataFrame:
419 def read_data(self, keyword: str) -> pd.DataFrame: 420 """ 421 データベースからデータを取得する 422 423 Args: 424 keyword (str): SQL選択キーワード 425 426 Returns: 427 pd.DataFrame: 集計結果 428 429 """ 430 query = self.query_modification(dbutil.query(keyword)) 431 432 if self.logging_verbose & 0x01: 433 print(f">>> params={self.placeholder()}") 434 print(f">>> SQL: {keyword} -> {self.database_file}\n{self.named_query(query)}") 435 436 try: 437 query_start_time = ExtDt().dt.timestamp() 438 df = pd.read_sql( 439 sql=query, 440 con=dbutil.connection(self.database_file), 441 params=self.placeholder(), 442 ) 443 query_end_time = ExtDt().dt.timestamp() 444 except pd.errors.DatabaseError as err: 445 logging.error("DatabaseError: %s", err) 446 logging.error("SQL: %s, DATABASE: %s", keyword, self.database_file) 447 logging.error("params=%s", self.placeholder()) 448 logging.error("query: %s", self.named_query(query)) 449 450 if self.logging_verbose & 0x02: 451 print("=" * 80) 452 print(df.to_string()) 453 454 logging.debug("SQL: %s, time: %s", keyword, query_end_time - query_start_time) 455 return df
データベースからデータを取得する
Arguments:
- keyword (str): SQL選択キーワード
Returns:
pd.DataFrame: 集計結果