cls.config
cls/config2.py
1""" 2cls/config2.py 3""" 4 5import logging 6import os 7import sys 8from configparser import ConfigParser, SectionProxy 9from dataclasses import dataclass, field 10from itertools import chain 11from math import ceil 12from pathlib import Path 13from types import UnionType 14from typing import Any, cast 15 16from cls.types import GradeTableDict 17 18 19class CommonMethodMixin: 20 """共通メソッド""" 21 _section: SectionProxy 22 23 def get(self, key: str, fallback: Any = None) -> Any: 24 """値の取得""" 25 return self._section.get(key, fallback) 26 27 def getint(self, key: str, fallback: int = 0) -> int: 28 """整数値の取得""" 29 return self._section.getint(key, fallback) 30 31 def getfloat(self, key: str, fallback: float = 0.0) -> float: 32 """数値の取得""" 33 return self._section.getfloat(key, fallback) 34 35 def getboolean(self, key: str, fallback: bool = False) -> bool: 36 """真偽値の取得""" 37 return cast(bool, self._section.getboolean(key, fallback)) 38 39 def getlist(self, key: str) -> list: 40 """リストの取得""" 41 return [x.strip() for x in self._section.get(key, "").split(",")] 42 43 def keys(self) -> list: 44 """キーリストの返却""" 45 return list(self._section.keys()) 46 47 def values(self) -> list: 48 """値リストの返却""" 49 return list(self._section.values()) 50 51 def items(self): 52 """ItemsViewを返却""" 53 return self._section.items() 54 55 def to_dict(self) -> dict[str, str]: 56 """辞書型に変換""" 57 return dict(self._section.items()) 58 59 60class BaseSection(CommonMethodMixin): 61 """共通処理""" 62 def __init__(self, outer, section_name: str): 63 parser = cast(ConfigParser, outer._parser) 64 if section_name not in parser: 65 return 66 self._section = parser[section_name] 67 68 self.initialization() 69 self.section = section_name # セクション名保持 70 logging.info("%s=%s", section_name, self.__dict__) 71 72 def initialization(self): 73 """設定ファイルから値の取り込み""" 74 self.__dict__.update(self.default_load()) 75 for k in self._section.keys(): 76 if k in self.__dict__: 77 match type(self.__dict__.get(k)): 78 case v_type if v_type is type(str()): 79 setattr(self, k, self._section.get(k, fallback=self.get(k))) 80 case v_type if v_type is type(int()): 81 setattr(self, k, self._section.getint(k, fallback=self.get(k))) 82 case v_type if v_type is type(float()): 83 setattr(self, k, self._section.getfloat(k, fallback=self.get(k))) 84 case v_type if v_type is type(bool()): 85 setattr(self, k, self._section.getboolean(k, fallback=self.get(k))) 86 case v_type if v_type is type([]): 87 v_list = [x.strip() for x in self._section.get(k, fallback=self.get(k)).split(",")] 88 setattr(self, k, v_list) 89 case v_type if isinstance(v_type, UnionType): 90 if set(v_type.__args__) == {str, type(None)}: 91 setattr(self, k, self._section.get(k, fallback=self.get(k))) 92 case v_type if v_type is type(None): 93 setattr(self, k, self._section.get(k, fallback=self.get(k))) 94 case _: 95 setattr(self, k, self.__dict__.get(k)) 96 97 def default_load(self) -> dict: 98 """クラス変数からデフォルト値の取り込み""" 99 return {k: v for k, v in self.__class__.__dict__.items() if not k.startswith("__") and not callable(v)} 100 101 def to_dict(self) -> dict: 102 """必要なパラメータを辞書型で返す 103 104 Returns: 105 dict: 返却値 106 """ 107 108 ret_dict = self.__dict__ 109 for key in ["_parser", "_section", "always_argument"]: 110 if key in ret_dict: 111 ret_dict.pop(key) 112 113 return ret_dict 114 115 116class MahjongSection(BaseSection): 117 """mahjongセクション初期値""" 118 rule_version: str = str() 119 """ルール判別識別子""" 120 origin_point: int = 250 121 """配給原点""" 122 return_point: int = 300 123 """返し点""" 124 rank_point: list = [30, 10, -10, -30] 125 """順位点""" 126 ignore_flying: bool = False 127 """トビカウント 128 - True: なし 129 - False: あり 130 """ 131 draw_split: bool = False 132 """同点時の順位点 133 - True: 山分けにする 134 - False: 席順で決める 135 """ 136 regulations_type2: list = [] 137 """メモで役満として扱う単語リスト(カンマ区切り)""" 138 139 def __init__(self, outer, section_name): 140 self._parser = cast(ConfigParser, outer._parser) 141 self.__dict__.update(super().default_load()) 142 super().__init__(self, section_name) 143 144 self.rank_point = list(map(int, self.rank_point)) # 数値化 145 146 147class SettingSection(BaseSection): 148 """settingセクション初期値""" 149 slash_command: str = "/mahjong" 150 """スラッシュコマンド名""" 151 thread_report: bool = True 152 """スレッド内にある得点報告を扱う""" 153 time_adjust: int = 12 154 """日付変更後、集計範囲に含める追加時間""" 155 guest_mark: str = "※" 156 """ゲスト無効時に未登録メンバーに付与する印""" 157 reaction_ok: str = "ok" 158 """DBに取り込んだ時に付けるリアクション""" 159 reaction_ng: str = "ng" 160 """DBに取り込んだが正確な値ではない可能性があるときに付けるリアクション""" 161 font_file: str = "ipaexg.ttf" 162 """グラフ描写に使用するフォントファイル""" 163 graph_style: str = "ggplot" 164 """グラフスタイル""" 165 work_dir: str = "work" 166 """生成したファイルを保存するディレクトリ""" 167 ignore_userid: list = [] 168 """投稿を無視するユーザのリスト(カンマ区切りで設定)""" 169 170 def __init__(self, outer, section_name: str): 171 self._parser = cast(ConfigParser, outer._parser) 172 super().__init__(self, section_name) 173 174 self.work_dir = os.path.realpath(os.path.join(outer.script_dir, self.work_dir)) 175 176 # フォントファイルチェック 177 for chk_dir in (outer.config_dir, outer.script_dir): 178 chk_path = str(os.path.realpath(os.path.join(chk_dir, self.font_file))) 179 if os.path.exists(chk_path): 180 self.font_file = chk_path 181 break 182 183 if chk_path != self.font_file: 184 logging.critical("The specified font file cannot be found.") 185 sys.exit(255) 186 187 logging.notice("fontfile: %s", self.font_file) # type: ignore 188 189 190class SearchSection(BaseSection): 191 """searchセクション初期値""" 192 keyword: str = "終局" 193 """成績記録キーワード""" 194 channel: str | None = None 195 """テータ突合時に成績記録ワードを検索するチャンネル名""" 196 after: int = 7 197 """データ突合時対象にする日数""" 198 wait: int = 180 199 """指定秒数以内にポストされているデータは突合対象から除外する""" 200 201 def __init__(self, outer, section_name: str): 202 self._parser = cast(ConfigParser, outer._parser) 203 super().__init__(self, section_name) 204 205 206class DatabaseSection(BaseSection): 207 """databaseセクション初期値""" 208 database_file: str = "mahjong.db" 209 """成績管理データベースファイル名""" 210 channel_limitations: list = [] 211 """SQLを実行できるチャンネルリスト""" 212 backup_dir: str | None = None 213 """バックアップ先ディレクトリ""" 214 215 def __init__(self, outer, section_name: str): 216 self._parser = cast(ConfigParser, outer._parser) 217 super().__init__(self, section_name) 218 219 self.database_file = os.path.realpath(os.path.join(outer.config_dir, self.database_file)) 220 if self.backup_dir: 221 self.backup_dir = os.path.realpath(os.path.join(outer.script_dir, self.backup_dir)) 222 223 logging.notice("database: %s", self.database_file) # type: ignore 224 225 226class MemberSection(BaseSection): 227 "memberセクション初期値""" 228 registration_limit: int = 255 229 """登録メンバー上限数""" 230 character_limit: int = 8 231 """名前に使用できる文字数""" 232 alias_limit: int = 16 233 """別名登録上限数""" 234 guest_name: str = "ゲスト" 235 """未登録メンバー名称""" 236 237 def __init__(self, outer, section_name: str): 238 self._parser = cast(ConfigParser, outer._parser) 239 super().__init__(self, section_name) 240 241 242class TeamSection(BaseSection): 243 """teamセクション初期値""" 244 registration_limit: int = 255 245 """登録チーム上限数""" 246 character_limit: int = 16 247 """チーム名に使用できる文字数""" 248 member_limit: int = 16 249 """チームに所属できるメンバー上限""" 250 friendly_fire: bool = True 251 """チームメイトが同卓しているゲームを集計対象に含めるか""" 252 253 def __init__(self, outer, section_name: str): 254 self._parser = cast(ConfigParser, outer._parser) 255 super().__init__(self, section_name) 256 257 258class AliasSection(BaseSection): 259 """aliasセクション初期値""" 260 results: list = [] 261 graph: list = [] 262 ranking: list = [] 263 report: list = [] 264 check: list = [] 265 download: list = [] 266 member: list = [] 267 add: list = [] 268 delete: list = [] # "del"はbuilt-inで使用 269 team_create: list = [] 270 team_del: list = [] 271 team_add: list = [] 272 team_remove: list = [] 273 team_list: list = [] 274 team_clear: list = [] 275 276 def __init__(self, outer, section_name: str): 277 self._parser = cast(ConfigParser, outer._parser) 278 super().__init__(self, section_name) 279 280 # デフォルト値として自身と同じ名前のコマンドを登録する # 281 parser = cast(ConfigParser, outer._parser) 282 for k in self.to_dict(): 283 x = getattr(self, k) 284 if isinstance(x, list): 285 x.append(k) 286 list_data = [x.strip() for x in str(parser.get("alias", "del", fallback="")).split(",")] + ["del"] 287 self.delete.extend(list_data) 288 289 290class CommentSection(BaseSection): 291 """commentセクション初期値""" 292 group_length: int = 0 293 """コメント検索時の集約文字数(固定指定)""" 294 search_word: str = "" 295 """コメント検索時の検索文字列(固定指定)""" 296 297 def __init__(self, outer, section_name: str): 298 self._parser = cast(ConfigParser, outer._parser) 299 super().__init__(self, section_name) 300 301 302class CommandWord(BaseSection): 303 """チャンネル内呼び出しキーワード初期値""" 304 help: str = "ヘルプ" 305 results: str = "麻雀成績" 306 graph: str = "麻雀グラフ" 307 ranking: str = "麻雀ランキング" 308 report: str = "麻雀成績レポート" 309 member: str = "メンバー一覧" 310 team: str = "チーム一覧" 311 remarks_word: str = "麻雀成績メモ" 312 check: str = "麻雀成績チェック" 313 314 def __init__(self, outer): 315 self._parser = cast(ConfigParser, outer._parser) 316 super().__init__(self, "") 317 318 self.help = self._parser.get("help", "commandword", fallback=CommandWord.help) 319 self.results = self._parser.get("results", "commandword", fallback=CommandWord.results) 320 self.graph = self._parser.get("graph", "commandword", fallback=CommandWord.graph) 321 self.ranking = self._parser.get("ranking", "commandword", fallback=CommandWord.ranking) 322 self.report = self._parser.get("report", "commandword", fallback=CommandWord.report) 323 self.member = self._parser.get("member", "commandword", fallback=CommandWord.member) 324 self.team = self._parser.get("team", "commandword", fallback=CommandWord.team) 325 self.remarks_word = self._parser.get("setting", "remarks_word", fallback=CommandWord.remarks_word) 326 self.check = self._parser.get("database", "commandword", fallback=CommandWord.check) 327 328 329class DropItems(BaseSection): 330 """非表示項目リスト""" 331 results: list = [] 332 ranking: list = [] 333 report: list = [] 334 335 def __init__(self, outer): 336 self._parser = cast(ConfigParser, outer._parser) 337 super().__init__(self, "") 338 339 self.results = [x.strip() for x in self._parser.get("results", "dropitems", fallback="").split(",")] 340 self.ranking = [x.strip() for x in self._parser.get("ranking", "dropitems", fallback="").split(",")] 341 self.report = [x.strip() for x in self._parser.get("report", "dropitems", fallback="").split(",")] 342 343 344class BadgeDisplay(BaseSection): 345 """バッジ表示""" 346 @dataclass 347 class BadgeGradeSpec: 348 """段位""" 349 display: bool = field(default=False) 350 table_name: str = field(default=str()) 351 table: GradeTableDict = field(default_factory=lambda: cast(GradeTableDict, dict)) 352 353 degree: bool = False 354 status: bool = False 355 grade: "BadgeGradeSpec" = BadgeGradeSpec() 356 357 def __init__(self, outer): 358 self._parser = cast(ConfigParser, outer._parser) 359 super().__init__(self, "") 360 361 self.degree = self._parser.getboolean("degree", "display", fallback=False) 362 self.status = self._parser.getboolean("status", "display", fallback=False) 363 self.grade.display = self._parser.getboolean("grade", "display", fallback=False) 364 self.grade.table_name = self._parser.get("grade", "table_name", fallback="") 365 366 367class SubCommand(BaseSection): 368 """サブコマンド共通クラス""" 369 section: str = "" 370 aggregation_range: str = "当日" 371 """検索範囲未指定時に使用される範囲""" 372 individual: bool = True 373 """個人/チーム集計切替フラグ 374 - True: 個人集計 375 - False: チーム集計 376 """ 377 all_player: bool = False 378 daily: bool = True 379 fourfold: bool = True 380 game_results: bool = False 381 guest_skip: bool = True 382 guest_skip2: bool = True 383 ranked: int = 3 384 score_comparisons: bool = False 385 """スコア比較""" 386 statistics: bool = False 387 """統計情報表示""" 388 stipulated: int = 1 389 """規定打数指定""" 390 stipulated_rate: float = 0.05 391 """規定打数計算レート""" 392 unregistered_replace: bool = True 393 """メンバー未登録プレイヤー名をゲストに置き換えるかフラグ 394 - True: 置き換える 395 - False: 置き換えない 396 """ 397 anonymous: bool = False 398 """匿名化フラグ""" 399 verbose: bool = False 400 """詳細情報出力フラグ""" 401 versus_matrix: bool = False 402 """対戦マトリックス表示""" 403 collection: str = "" 404 search_word: str = "" 405 group_length: int = 0 406 always_argument: list = [] 407 """オプションとして常に付与される文字列(カンマ区切り)""" 408 format: str = "" 409 filename: str = "" 410 interval: int = 80 411 412 def __init__(self, outer, section_name: str): 413 self._parser = cast(ConfigParser, outer._parser) 414 self.section = section_name 415 super().__init__(self, section_name) 416 417 def stipulated_calculation(self, game_count: int) -> int: 418 """規定打数をゲーム数から計算 419 420 Args: 421 game_count (int): 指定ゲーム数 422 423 Returns: 424 int: 規定ゲーム数 425 """ 426 427 return int(ceil(game_count * self.stipulated_rate) + 1) 428 429 430class AppConfig: 431 """コンフィグ解析クラス""" 432 def __init__(self, path: str): 433 path = os.path.realpath(path) 434 try: 435 self._parser = ConfigParser() 436 self._parser.read(path, encoding="utf-8") 437 logging.notice("configfile: %s", path) # type: ignore 438 except Exception as e: 439 raise RuntimeError(e) from e 440 441 # 必須セクションチェック 442 for x in ("mahjong", "setting"): 443 if x not in self._parser.sections(): 444 logging.critical("Required section not found. (%s)", x) 445 sys.exit(255) 446 447 # オプションセクションチェック 448 option_sections = [ 449 "results", 450 "graph", 451 "ranking", 452 "report", 453 "database", 454 "search", 455 "alias", 456 "member", 457 "team", 458 "comment", 459 "regulations", 460 "help", 461 ] 462 for x in option_sections: 463 if x not in self._parser.sections(): 464 self._parser.add_section(x) 465 466 # set base directory 467 self.script_dir = os.path.realpath(str(Path(__file__).resolve().parents[1])) 468 self.config_dir = os.path.dirname(os.path.realpath(str(path))) 469 logging.info("script_dir=%s, config_dir=%s", self.script_dir, self.config_dir) 470 471 # 設定値取り込み 472 self.mahjong = MahjongSection(self, "mahjong") 473 self.setting = SettingSection(self, "setting") 474 self.search = SearchSection(self, "search") 475 self.db = DatabaseSection(self, "database") 476 self.member = MemberSection(self, "member") 477 self.team = TeamSection(self, "team") 478 self.alias = AliasSection(self, "alias") 479 self.comment = CommentSection(self, "comment") 480 self.cw = CommandWord(self) # チャンネル内呼び出しキーワード 481 self.dropitems = DropItems(self) # 非表示項目 482 self.badge = BadgeDisplay(self) # バッジ表示 483 484 # サブコマンドデフォルト 485 self.results = SubCommand(self, "results") 486 self.graph = SubCommand(self, "graph") 487 self.ranking = SubCommand(self, "ranking") 488 self.report = SubCommand(self, "report") 489 490 # 共通設定値 491 self.undefined_word: int = 0 492 self.aggregate_unit: str = "" 493 494 def word_list(self) -> list: 495 """設定されている値、キーワードをリスト化する 496 497 Returns: 498 list: リスト化されたキーワード 499 """ 500 501 words: list = [] 502 503 words.append([self.setting.slash_command]) 504 words.append([self.search.keyword]) 505 506 for x in self.cw.to_dict().values(): 507 words.append([x]) 508 509 for k, v in self.alias.to_dict().items(): 510 if isinstance(v, list): 511 words.append([k]) 512 words.append(v) 513 514 words = list(set(chain.from_iterable(words))) 515 words = ["del" if x == "delete" else x for x in words] 516 words = [x for x in words if x != ""] 517 518 return words
class
CommonMethodMixin:
20class CommonMethodMixin: 21 """共通メソッド""" 22 _section: SectionProxy 23 24 def get(self, key: str, fallback: Any = None) -> Any: 25 """値の取得""" 26 return self._section.get(key, fallback) 27 28 def getint(self, key: str, fallback: int = 0) -> int: 29 """整数値の取得""" 30 return self._section.getint(key, fallback) 31 32 def getfloat(self, key: str, fallback: float = 0.0) -> float: 33 """数値の取得""" 34 return self._section.getfloat(key, fallback) 35 36 def getboolean(self, key: str, fallback: bool = False) -> bool: 37 """真偽値の取得""" 38 return cast(bool, self._section.getboolean(key, fallback)) 39 40 def getlist(self, key: str) -> list: 41 """リストの取得""" 42 return [x.strip() for x in self._section.get(key, "").split(",")] 43 44 def keys(self) -> list: 45 """キーリストの返却""" 46 return list(self._section.keys()) 47 48 def values(self) -> list: 49 """値リストの返却""" 50 return list(self._section.values()) 51 52 def items(self): 53 """ItemsViewを返却""" 54 return self._section.items() 55 56 def to_dict(self) -> dict[str, str]: 57 """辞書型に変換""" 58 return dict(self._section.items())
共通メソッド
def
get(self, key: str, fallback: Any = None) -> Any:
24 def get(self, key: str, fallback: Any = None) -> Any: 25 """値の取得""" 26 return self._section.get(key, fallback)
値の取得
def
getint(self, key: str, fallback: int = 0) -> int:
28 def getint(self, key: str, fallback: int = 0) -> int: 29 """整数値の取得""" 30 return self._section.getint(key, fallback)
整数値の取得
def
getfloat(self, key: str, fallback: float = 0.0) -> float:
32 def getfloat(self, key: str, fallback: float = 0.0) -> float: 33 """数値の取得""" 34 return self._section.getfloat(key, fallback)
数値の取得
def
getboolean(self, key: str, fallback: bool = False) -> bool:
36 def getboolean(self, key: str, fallback: bool = False) -> bool: 37 """真偽値の取得""" 38 return cast(bool, self._section.getboolean(key, fallback))
真偽値の取得
def
getlist(self, key: str) -> list:
40 def getlist(self, key: str) -> list: 41 """リストの取得""" 42 return [x.strip() for x in self._section.get(key, "").split(",")]
リストの取得
61class BaseSection(CommonMethodMixin): 62 """共通処理""" 63 def __init__(self, outer, section_name: str): 64 parser = cast(ConfigParser, outer._parser) 65 if section_name not in parser: 66 return 67 self._section = parser[section_name] 68 69 self.initialization() 70 self.section = section_name # セクション名保持 71 logging.info("%s=%s", section_name, self.__dict__) 72 73 def initialization(self): 74 """設定ファイルから値の取り込み""" 75 self.__dict__.update(self.default_load()) 76 for k in self._section.keys(): 77 if k in self.__dict__: 78 match type(self.__dict__.get(k)): 79 case v_type if v_type is type(str()): 80 setattr(self, k, self._section.get(k, fallback=self.get(k))) 81 case v_type if v_type is type(int()): 82 setattr(self, k, self._section.getint(k, fallback=self.get(k))) 83 case v_type if v_type is type(float()): 84 setattr(self, k, self._section.getfloat(k, fallback=self.get(k))) 85 case v_type if v_type is type(bool()): 86 setattr(self, k, self._section.getboolean(k, fallback=self.get(k))) 87 case v_type if v_type is type([]): 88 v_list = [x.strip() for x in self._section.get(k, fallback=self.get(k)).split(",")] 89 setattr(self, k, v_list) 90 case v_type if isinstance(v_type, UnionType): 91 if set(v_type.__args__) == {str, type(None)}: 92 setattr(self, k, self._section.get(k, fallback=self.get(k))) 93 case v_type if v_type is type(None): 94 setattr(self, k, self._section.get(k, fallback=self.get(k))) 95 case _: 96 setattr(self, k, self.__dict__.get(k)) 97 98 def default_load(self) -> dict: 99 """クラス変数からデフォルト値の取り込み""" 100 return {k: v for k, v in self.__class__.__dict__.items() if not k.startswith("__") and not callable(v)} 101 102 def to_dict(self) -> dict: 103 """必要なパラメータを辞書型で返す 104 105 Returns: 106 dict: 返却値 107 """ 108 109 ret_dict = self.__dict__ 110 for key in ["_parser", "_section", "always_argument"]: 111 if key in ret_dict: 112 ret_dict.pop(key) 113 114 return ret_dict
共通処理
BaseSection(outer, section_name: str)
63 def __init__(self, outer, section_name: str): 64 parser = cast(ConfigParser, outer._parser) 65 if section_name not in parser: 66 return 67 self._section = parser[section_name] 68 69 self.initialization() 70 self.section = section_name # セクション名保持 71 logging.info("%s=%s", section_name, self.__dict__)
def
initialization(self):
73 def initialization(self): 74 """設定ファイルから値の取り込み""" 75 self.__dict__.update(self.default_load()) 76 for k in self._section.keys(): 77 if k in self.__dict__: 78 match type(self.__dict__.get(k)): 79 case v_type if v_type is type(str()): 80 setattr(self, k, self._section.get(k, fallback=self.get(k))) 81 case v_type if v_type is type(int()): 82 setattr(self, k, self._section.getint(k, fallback=self.get(k))) 83 case v_type if v_type is type(float()): 84 setattr(self, k, self._section.getfloat(k, fallback=self.get(k))) 85 case v_type if v_type is type(bool()): 86 setattr(self, k, self._section.getboolean(k, fallback=self.get(k))) 87 case v_type if v_type is type([]): 88 v_list = [x.strip() for x in self._section.get(k, fallback=self.get(k)).split(",")] 89 setattr(self, k, v_list) 90 case v_type if isinstance(v_type, UnionType): 91 if set(v_type.__args__) == {str, type(None)}: 92 setattr(self, k, self._section.get(k, fallback=self.get(k))) 93 case v_type if v_type is type(None): 94 setattr(self, k, self._section.get(k, fallback=self.get(k))) 95 case _: 96 setattr(self, k, self.__dict__.get(k))
設定ファイルから値の取り込み
def
default_load(self) -> dict:
98 def default_load(self) -> dict: 99 """クラス変数からデフォルト値の取り込み""" 100 return {k: v for k, v in self.__class__.__dict__.items() if not k.startswith("__") and not callable(v)}
クラス変数からデフォルト値の取り込み
def
to_dict(self) -> dict:
102 def to_dict(self) -> dict: 103 """必要なパラメータを辞書型で返す 104 105 Returns: 106 dict: 返却値 107 """ 108 109 ret_dict = self.__dict__ 110 for key in ["_parser", "_section", "always_argument"]: 111 if key in ret_dict: 112 ret_dict.pop(key) 113 114 return ret_dict
必要なパラメータを辞書型で返す
Returns:
dict: 返却値
Inherited Members
117class MahjongSection(BaseSection): 118 """mahjongセクション初期値""" 119 rule_version: str = str() 120 """ルール判別識別子""" 121 origin_point: int = 250 122 """配給原点""" 123 return_point: int = 300 124 """返し点""" 125 rank_point: list = [30, 10, -10, -30] 126 """順位点""" 127 ignore_flying: bool = False 128 """トビカウント 129 - True: なし 130 - False: あり 131 """ 132 draw_split: bool = False 133 """同点時の順位点 134 - True: 山分けにする 135 - False: 席順で決める 136 """ 137 regulations_type2: list = [] 138 """メモで役満として扱う単語リスト(カンマ区切り)""" 139 140 def __init__(self, outer, section_name): 141 self._parser = cast(ConfigParser, outer._parser) 142 self.__dict__.update(super().default_load()) 143 super().__init__(self, section_name) 144 145 self.rank_point = list(map(int, self.rank_point)) # 数値化
mahjongセクション初期値
Inherited Members
148class SettingSection(BaseSection): 149 """settingセクション初期値""" 150 slash_command: str = "/mahjong" 151 """スラッシュコマンド名""" 152 thread_report: bool = True 153 """スレッド内にある得点報告を扱う""" 154 time_adjust: int = 12 155 """日付変更後、集計範囲に含める追加時間""" 156 guest_mark: str = "※" 157 """ゲスト無効時に未登録メンバーに付与する印""" 158 reaction_ok: str = "ok" 159 """DBに取り込んだ時に付けるリアクション""" 160 reaction_ng: str = "ng" 161 """DBに取り込んだが正確な値ではない可能性があるときに付けるリアクション""" 162 font_file: str = "ipaexg.ttf" 163 """グラフ描写に使用するフォントファイル""" 164 graph_style: str = "ggplot" 165 """グラフスタイル""" 166 work_dir: str = "work" 167 """生成したファイルを保存するディレクトリ""" 168 ignore_userid: list = [] 169 """投稿を無視するユーザのリスト(カンマ区切りで設定)""" 170 171 def __init__(self, outer, section_name: str): 172 self._parser = cast(ConfigParser, outer._parser) 173 super().__init__(self, section_name) 174 175 self.work_dir = os.path.realpath(os.path.join(outer.script_dir, self.work_dir)) 176 177 # フォントファイルチェック 178 for chk_dir in (outer.config_dir, outer.script_dir): 179 chk_path = str(os.path.realpath(os.path.join(chk_dir, self.font_file))) 180 if os.path.exists(chk_path): 181 self.font_file = chk_path 182 break 183 184 if chk_path != self.font_file: 185 logging.critical("The specified font file cannot be found.") 186 sys.exit(255) 187 188 logging.notice("fontfile: %s", self.font_file) # type: ignore
settingセクション初期値
SettingSection(outer, section_name: str)
171 def __init__(self, outer, section_name: str): 172 self._parser = cast(ConfigParser, outer._parser) 173 super().__init__(self, section_name) 174 175 self.work_dir = os.path.realpath(os.path.join(outer.script_dir, self.work_dir)) 176 177 # フォントファイルチェック 178 for chk_dir in (outer.config_dir, outer.script_dir): 179 chk_path = str(os.path.realpath(os.path.join(chk_dir, self.font_file))) 180 if os.path.exists(chk_path): 181 self.font_file = chk_path 182 break 183 184 if chk_path != self.font_file: 185 logging.critical("The specified font file cannot be found.") 186 sys.exit(255) 187 188 logging.notice("fontfile: %s", self.font_file) # type: ignore
Inherited Members
191class SearchSection(BaseSection): 192 """searchセクション初期値""" 193 keyword: str = "終局" 194 """成績記録キーワード""" 195 channel: str | None = None 196 """テータ突合時に成績記録ワードを検索するチャンネル名""" 197 after: int = 7 198 """データ突合時対象にする日数""" 199 wait: int = 180 200 """指定秒数以内にポストされているデータは突合対象から除外する""" 201 202 def __init__(self, outer, section_name: str): 203 self._parser = cast(ConfigParser, outer._parser) 204 super().__init__(self, section_name)
searchセクション初期値
Inherited Members
207class DatabaseSection(BaseSection): 208 """databaseセクション初期値""" 209 database_file: str = "mahjong.db" 210 """成績管理データベースファイル名""" 211 channel_limitations: list = [] 212 """SQLを実行できるチャンネルリスト""" 213 backup_dir: str | None = None 214 """バックアップ先ディレクトリ""" 215 216 def __init__(self, outer, section_name: str): 217 self._parser = cast(ConfigParser, outer._parser) 218 super().__init__(self, section_name) 219 220 self.database_file = os.path.realpath(os.path.join(outer.config_dir, self.database_file)) 221 if self.backup_dir: 222 self.backup_dir = os.path.realpath(os.path.join(outer.script_dir, self.backup_dir)) 223 224 logging.notice("database: %s", self.database_file) # type: ignore
databaseセクション初期値
DatabaseSection(outer, section_name: str)
216 def __init__(self, outer, section_name: str): 217 self._parser = cast(ConfigParser, outer._parser) 218 super().__init__(self, section_name) 219 220 self.database_file = os.path.realpath(os.path.join(outer.config_dir, self.database_file)) 221 if self.backup_dir: 222 self.backup_dir = os.path.realpath(os.path.join(outer.script_dir, self.backup_dir)) 223 224 logging.notice("database: %s", self.database_file) # type: ignore
Inherited Members
227class MemberSection(BaseSection): 228 "memberセクション初期値""" 229 registration_limit: int = 255 230 """登録メンバー上限数""" 231 character_limit: int = 8 232 """名前に使用できる文字数""" 233 alias_limit: int = 16 234 """別名登録上限数""" 235 guest_name: str = "ゲスト" 236 """未登録メンバー名称""" 237 238 def __init__(self, outer, section_name: str): 239 self._parser = cast(ConfigParser, outer._parser) 240 super().__init__(self, section_name)
memberセクション初期値
Inherited Members
243class TeamSection(BaseSection): 244 """teamセクション初期値""" 245 registration_limit: int = 255 246 """登録チーム上限数""" 247 character_limit: int = 16 248 """チーム名に使用できる文字数""" 249 member_limit: int = 16 250 """チームに所属できるメンバー上限""" 251 friendly_fire: bool = True 252 """チームメイトが同卓しているゲームを集計対象に含めるか""" 253 254 def __init__(self, outer, section_name: str): 255 self._parser = cast(ConfigParser, outer._parser) 256 super().__init__(self, section_name)
teamセクション初期値
Inherited Members
259class AliasSection(BaseSection): 260 """aliasセクション初期値""" 261 results: list = [] 262 graph: list = [] 263 ranking: list = [] 264 report: list = [] 265 check: list = [] 266 download: list = [] 267 member: list = [] 268 add: list = [] 269 delete: list = [] # "del"はbuilt-inで使用 270 team_create: list = [] 271 team_del: list = [] 272 team_add: list = [] 273 team_remove: list = [] 274 team_list: list = [] 275 team_clear: list = [] 276 277 def __init__(self, outer, section_name: str): 278 self._parser = cast(ConfigParser, outer._parser) 279 super().__init__(self, section_name) 280 281 # デフォルト値として自身と同じ名前のコマンドを登録する # 282 parser = cast(ConfigParser, outer._parser) 283 for k in self.to_dict(): 284 x = getattr(self, k) 285 if isinstance(x, list): 286 x.append(k) 287 list_data = [x.strip() for x in str(parser.get("alias", "del", fallback="")).split(",")] + ["del"] 288 self.delete.extend(list_data)
aliasセクション初期値
AliasSection(outer, section_name: str)
277 def __init__(self, outer, section_name: str): 278 self._parser = cast(ConfigParser, outer._parser) 279 super().__init__(self, section_name) 280 281 # デフォルト値として自身と同じ名前のコマンドを登録する # 282 parser = cast(ConfigParser, outer._parser) 283 for k in self.to_dict(): 284 x = getattr(self, k) 285 if isinstance(x, list): 286 x.append(k) 287 list_data = [x.strip() for x in str(parser.get("alias", "del", fallback="")).split(",")] + ["del"] 288 self.delete.extend(list_data)
Inherited Members
291class CommentSection(BaseSection): 292 """commentセクション初期値""" 293 group_length: int = 0 294 """コメント検索時の集約文字数(固定指定)""" 295 search_word: str = "" 296 """コメント検索時の検索文字列(固定指定)""" 297 298 def __init__(self, outer, section_name: str): 299 self._parser = cast(ConfigParser, outer._parser) 300 super().__init__(self, section_name)
commentセクション初期値
Inherited Members
303class CommandWord(BaseSection): 304 """チャンネル内呼び出しキーワード初期値""" 305 help: str = "ヘルプ" 306 results: str = "麻雀成績" 307 graph: str = "麻雀グラフ" 308 ranking: str = "麻雀ランキング" 309 report: str = "麻雀成績レポート" 310 member: str = "メンバー一覧" 311 team: str = "チーム一覧" 312 remarks_word: str = "麻雀成績メモ" 313 check: str = "麻雀成績チェック" 314 315 def __init__(self, outer): 316 self._parser = cast(ConfigParser, outer._parser) 317 super().__init__(self, "") 318 319 self.help = self._parser.get("help", "commandword", fallback=CommandWord.help) 320 self.results = self._parser.get("results", "commandword", fallback=CommandWord.results) 321 self.graph = self._parser.get("graph", "commandword", fallback=CommandWord.graph) 322 self.ranking = self._parser.get("ranking", "commandword", fallback=CommandWord.ranking) 323 self.report = self._parser.get("report", "commandword", fallback=CommandWord.report) 324 self.member = self._parser.get("member", "commandword", fallback=CommandWord.member) 325 self.team = self._parser.get("team", "commandword", fallback=CommandWord.team) 326 self.remarks_word = self._parser.get("setting", "remarks_word", fallback=CommandWord.remarks_word) 327 self.check = self._parser.get("database", "commandword", fallback=CommandWord.check)
チャンネル内呼び出しキーワード初期値
CommandWord(outer)
315 def __init__(self, outer): 316 self._parser = cast(ConfigParser, outer._parser) 317 super().__init__(self, "") 318 319 self.help = self._parser.get("help", "commandword", fallback=CommandWord.help) 320 self.results = self._parser.get("results", "commandword", fallback=CommandWord.results) 321 self.graph = self._parser.get("graph", "commandword", fallback=CommandWord.graph) 322 self.ranking = self._parser.get("ranking", "commandword", fallback=CommandWord.ranking) 323 self.report = self._parser.get("report", "commandword", fallback=CommandWord.report) 324 self.member = self._parser.get("member", "commandword", fallback=CommandWord.member) 325 self.team = self._parser.get("team", "commandword", fallback=CommandWord.team) 326 self.remarks_word = self._parser.get("setting", "remarks_word", fallback=CommandWord.remarks_word) 327 self.check = self._parser.get("database", "commandword", fallback=CommandWord.check)
Inherited Members
330class DropItems(BaseSection): 331 """非表示項目リスト""" 332 results: list = [] 333 ranking: list = [] 334 report: list = [] 335 336 def __init__(self, outer): 337 self._parser = cast(ConfigParser, outer._parser) 338 super().__init__(self, "") 339 340 self.results = [x.strip() for x in self._parser.get("results", "dropitems", fallback="").split(",")] 341 self.ranking = [x.strip() for x in self._parser.get("ranking", "dropitems", fallback="").split(",")] 342 self.report = [x.strip() for x in self._parser.get("report", "dropitems", fallback="").split(",")]
非表示項目リスト
DropItems(outer)
336 def __init__(self, outer): 337 self._parser = cast(ConfigParser, outer._parser) 338 super().__init__(self, "") 339 340 self.results = [x.strip() for x in self._parser.get("results", "dropitems", fallback="").split(",")] 341 self.ranking = [x.strip() for x in self._parser.get("ranking", "dropitems", fallback="").split(",")] 342 self.report = [x.strip() for x in self._parser.get("report", "dropitems", fallback="").split(",")]
Inherited Members
345class BadgeDisplay(BaseSection): 346 """バッジ表示""" 347 @dataclass 348 class BadgeGradeSpec: 349 """段位""" 350 display: bool = field(default=False) 351 table_name: str = field(default=str()) 352 table: GradeTableDict = field(default_factory=lambda: cast(GradeTableDict, dict)) 353 354 degree: bool = False 355 status: bool = False 356 grade: "BadgeGradeSpec" = BadgeGradeSpec() 357 358 def __init__(self, outer): 359 self._parser = cast(ConfigParser, outer._parser) 360 super().__init__(self, "") 361 362 self.degree = self._parser.getboolean("degree", "display", fallback=False) 363 self.status = self._parser.getboolean("status", "display", fallback=False) 364 self.grade.display = self._parser.getboolean("grade", "display", fallback=False) 365 self.grade.table_name = self._parser.get("grade", "table_name", fallback="")
バッジ表示
BadgeDisplay(outer)
358 def __init__(self, outer): 359 self._parser = cast(ConfigParser, outer._parser) 360 super().__init__(self, "") 361 362 self.degree = self._parser.getboolean("degree", "display", fallback=False) 363 self.status = self._parser.getboolean("status", "display", fallback=False) 364 self.grade.display = self._parser.getboolean("grade", "display", fallback=False) 365 self.grade.table_name = self._parser.get("grade", "table_name", fallback="")
grade: BadgeDisplay.BadgeGradeSpec =
BadgeDisplay.BadgeGradeSpec(display=False, table_name='', table=<class 'dict'>)
Inherited Members
@dataclass
class
BadgeDisplay.BadgeGradeSpec:
347 @dataclass 348 class BadgeGradeSpec: 349 """段位""" 350 display: bool = field(default=False) 351 table_name: str = field(default=str()) 352 table: GradeTableDict = field(default_factory=lambda: cast(GradeTableDict, dict))
段位
BadgeDisplay.BadgeGradeSpec( display: bool = False, table_name: str = '', table: cls.types.GradeTableDict = <factory>)
table: cls.types.GradeTableDict
368class SubCommand(BaseSection): 369 """サブコマンド共通クラス""" 370 section: str = "" 371 aggregation_range: str = "当日" 372 """検索範囲未指定時に使用される範囲""" 373 individual: bool = True 374 """個人/チーム集計切替フラグ 375 - True: 個人集計 376 - False: チーム集計 377 """ 378 all_player: bool = False 379 daily: bool = True 380 fourfold: bool = True 381 game_results: bool = False 382 guest_skip: bool = True 383 guest_skip2: bool = True 384 ranked: int = 3 385 score_comparisons: bool = False 386 """スコア比較""" 387 statistics: bool = False 388 """統計情報表示""" 389 stipulated: int = 1 390 """規定打数指定""" 391 stipulated_rate: float = 0.05 392 """規定打数計算レート""" 393 unregistered_replace: bool = True 394 """メンバー未登録プレイヤー名をゲストに置き換えるかフラグ 395 - True: 置き換える 396 - False: 置き換えない 397 """ 398 anonymous: bool = False 399 """匿名化フラグ""" 400 verbose: bool = False 401 """詳細情報出力フラグ""" 402 versus_matrix: bool = False 403 """対戦マトリックス表示""" 404 collection: str = "" 405 search_word: str = "" 406 group_length: int = 0 407 always_argument: list = [] 408 """オプションとして常に付与される文字列(カンマ区切り)""" 409 format: str = "" 410 filename: str = "" 411 interval: int = 80 412 413 def __init__(self, outer, section_name: str): 414 self._parser = cast(ConfigParser, outer._parser) 415 self.section = section_name 416 super().__init__(self, section_name) 417 418 def stipulated_calculation(self, game_count: int) -> int: 419 """規定打数をゲーム数から計算 420 421 Args: 422 game_count (int): 指定ゲーム数 423 424 Returns: 425 int: 規定ゲーム数 426 """ 427 428 return int(ceil(game_count * self.stipulated_rate) + 1)
サブコマンド共通クラス
def
stipulated_calculation(self, game_count: int) -> int:
418 def stipulated_calculation(self, game_count: int) -> int: 419 """規定打数をゲーム数から計算 420 421 Args: 422 game_count (int): 指定ゲーム数 423 424 Returns: 425 int: 規定ゲーム数 426 """ 427 428 return int(ceil(game_count * self.stipulated_rate) + 1)
規定打数をゲーム数から計算
Arguments:
- game_count (int): 指定ゲーム数
Returns:
int: 規定ゲーム数
Inherited Members
class
AppConfig:
431class AppConfig: 432 """コンフィグ解析クラス""" 433 def __init__(self, path: str): 434 path = os.path.realpath(path) 435 try: 436 self._parser = ConfigParser() 437 self._parser.read(path, encoding="utf-8") 438 logging.notice("configfile: %s", path) # type: ignore 439 except Exception as e: 440 raise RuntimeError(e) from e 441 442 # 必須セクションチェック 443 for x in ("mahjong", "setting"): 444 if x not in self._parser.sections(): 445 logging.critical("Required section not found. (%s)", x) 446 sys.exit(255) 447 448 # オプションセクションチェック 449 option_sections = [ 450 "results", 451 "graph", 452 "ranking", 453 "report", 454 "database", 455 "search", 456 "alias", 457 "member", 458 "team", 459 "comment", 460 "regulations", 461 "help", 462 ] 463 for x in option_sections: 464 if x not in self._parser.sections(): 465 self._parser.add_section(x) 466 467 # set base directory 468 self.script_dir = os.path.realpath(str(Path(__file__).resolve().parents[1])) 469 self.config_dir = os.path.dirname(os.path.realpath(str(path))) 470 logging.info("script_dir=%s, config_dir=%s", self.script_dir, self.config_dir) 471 472 # 設定値取り込み 473 self.mahjong = MahjongSection(self, "mahjong") 474 self.setting = SettingSection(self, "setting") 475 self.search = SearchSection(self, "search") 476 self.db = DatabaseSection(self, "database") 477 self.member = MemberSection(self, "member") 478 self.team = TeamSection(self, "team") 479 self.alias = AliasSection(self, "alias") 480 self.comment = CommentSection(self, "comment") 481 self.cw = CommandWord(self) # チャンネル内呼び出しキーワード 482 self.dropitems = DropItems(self) # 非表示項目 483 self.badge = BadgeDisplay(self) # バッジ表示 484 485 # サブコマンドデフォルト 486 self.results = SubCommand(self, "results") 487 self.graph = SubCommand(self, "graph") 488 self.ranking = SubCommand(self, "ranking") 489 self.report = SubCommand(self, "report") 490 491 # 共通設定値 492 self.undefined_word: int = 0 493 self.aggregate_unit: str = "" 494 495 def word_list(self) -> list: 496 """設定されている値、キーワードをリスト化する 497 498 Returns: 499 list: リスト化されたキーワード 500 """ 501 502 words: list = [] 503 504 words.append([self.setting.slash_command]) 505 words.append([self.search.keyword]) 506 507 for x in self.cw.to_dict().values(): 508 words.append([x]) 509 510 for k, v in self.alias.to_dict().items(): 511 if isinstance(v, list): 512 words.append([k]) 513 words.append(v) 514 515 words = list(set(chain.from_iterable(words))) 516 words = ["del" if x == "delete" else x for x in words] 517 words = [x for x in words if x != ""] 518 519 return words
コンフィグ解析クラス
AppConfig(path: str)
433 def __init__(self, path: str): 434 path = os.path.realpath(path) 435 try: 436 self._parser = ConfigParser() 437 self._parser.read(path, encoding="utf-8") 438 logging.notice("configfile: %s", path) # type: ignore 439 except Exception as e: 440 raise RuntimeError(e) from e 441 442 # 必須セクションチェック 443 for x in ("mahjong", "setting"): 444 if x not in self._parser.sections(): 445 logging.critical("Required section not found. (%s)", x) 446 sys.exit(255) 447 448 # オプションセクションチェック 449 option_sections = [ 450 "results", 451 "graph", 452 "ranking", 453 "report", 454 "database", 455 "search", 456 "alias", 457 "member", 458 "team", 459 "comment", 460 "regulations", 461 "help", 462 ] 463 for x in option_sections: 464 if x not in self._parser.sections(): 465 self._parser.add_section(x) 466 467 # set base directory 468 self.script_dir = os.path.realpath(str(Path(__file__).resolve().parents[1])) 469 self.config_dir = os.path.dirname(os.path.realpath(str(path))) 470 logging.info("script_dir=%s, config_dir=%s", self.script_dir, self.config_dir) 471 472 # 設定値取り込み 473 self.mahjong = MahjongSection(self, "mahjong") 474 self.setting = SettingSection(self, "setting") 475 self.search = SearchSection(self, "search") 476 self.db = DatabaseSection(self, "database") 477 self.member = MemberSection(self, "member") 478 self.team = TeamSection(self, "team") 479 self.alias = AliasSection(self, "alias") 480 self.comment = CommentSection(self, "comment") 481 self.cw = CommandWord(self) # チャンネル内呼び出しキーワード 482 self.dropitems = DropItems(self) # 非表示項目 483 self.badge = BadgeDisplay(self) # バッジ表示 484 485 # サブコマンドデフォルト 486 self.results = SubCommand(self, "results") 487 self.graph = SubCommand(self, "graph") 488 self.ranking = SubCommand(self, "ranking") 489 self.report = SubCommand(self, "report") 490 491 # 共通設定値 492 self.undefined_word: int = 0 493 self.aggregate_unit: str = ""
def
word_list(self) -> list:
495 def word_list(self) -> list: 496 """設定されている値、キーワードをリスト化する 497 498 Returns: 499 list: リスト化されたキーワード 500 """ 501 502 words: list = [] 503 504 words.append([self.setting.slash_command]) 505 words.append([self.search.keyword]) 506 507 for x in self.cw.to_dict().values(): 508 words.append([x]) 509 510 for k, v in self.alias.to_dict().items(): 511 if isinstance(v, list): 512 words.append([k]) 513 words.append(v) 514 515 words = list(set(chain.from_iterable(words))) 516 words = ["del" if x == "delete" else x for x in words] 517 words = [x for x in words if x != ""] 518 519 return words
設定されている値、キーワードをリスト化する
Returns:
list: リスト化されたキーワード