#!/usr/bin/env python """ イベントユーザー登録スクリプト 外部システムAPI仕様書.mdを前提に、ユーザーデータCSVから、 各ユーザーごとにユーザー登録、チーム登録、エントリー登録、イベント参加を行う docker composeで実施するPythonスクリプト 使用方法: python register_event_users.py --event_code 大垣2509 ユーザーデータのCSVは以下の項目を持つ: 部門別数,時間,部門,チーム名,メール,パスワード,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,, """ import os import sys import csv import requests import argparse import logging from datetime import datetime, date import time import json from typing import Dict, List, Optional, Tuple # ログ設定 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('register_event_users.log'), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) class EventUserRegistration: def __init__(self, event_code: str, base_url: str = "http://localhost:8000", dry_run: bool = False): """ イベントユーザー登録クラス Args: event_code: イベントコード(例: 大垣2509) base_url: APIベースURL dry_run: テスト実行フラグ """ self.event_code = event_code self.base_url = base_url.rstrip('/') self.dry_run = dry_run self.session = requests.Session() self.admin_token = None # 統計情報 self.stats = { 'processed_teams': 0, 'users_created': 0, 'users_updated': 0, 'teams_registered': 0, 'entries_created': 0, 'participations_created': 0, 'errors': [] } logger.info(f"Event User Registration initialized for event: {event_code}") if dry_run: logger.info("DRY RUN MODE - No actual API calls will be made") def get_or_create_user(self, email: str, password: str, firstname: str, lastname: str, date_of_birth: str, phone: str) -> Tuple[bool, Optional[str], Optional[str]]: """ メールアドレスをキーに既存ユーザーを取得、存在しなければ新規作成 Args: email: メールアドレス password: パスワード firstname: 名前 lastname: 姓 date_of_birth: 生年月日 (YYYY/MM/DD形式) phone: 電話番号 Returns: Tuple[success, user_id, token] """ if self.dry_run: logger.info(f"[DRY RUN] Would get or create user: {email}") return True, "dummy_user_id", "dummy_token" try: # まずログインを試行して既存ユーザーかチェック login_data = { "identifier": email, "password": password } response = self.session.post(f"{self.base_url}/login/", json=login_data) if response.status_code == 200: # 既存ユーザーの場合、パスワード更新(実際にはパスワード更新APIが必要) result = response.json() token = result.get('token') user_id = result.get('user', {}).get('id') logger.info(f"既存ユーザーでログイン成功: {email}") self.stats['users_updated'] += 1 return True, str(user_id), token elif response.status_code == 401: # ユーザーが存在しないか、パスワードが間違っている場合、新規登録を試行 return self._create_new_user(email, password, firstname, lastname, date_of_birth) else: logger.error(f"ログイン試行でエラー: {response.status_code} - {response.text}") return False, None, None except Exception as e: logger.error(f"ユーザー認証エラー: {str(e)}") return False, None, None def _create_new_user(self, email: str, password: str, firstname: str, lastname: str, date_of_birth: str) -> Tuple[bool, Optional[str], Optional[str]]: """ 新規ユーザーを作成 """ try: # 生年月日をYYYY-MM-DD形式に変換 if '/' in date_of_birth: date_parts = date_of_birth.split('/') if len(date_parts) == 3: birth_date = f"{date_parts[0]}-{date_parts[1].zfill(2)}-{date_parts[2].zfill(2)}" else: birth_date = "1990-01-01" # デフォルト値 else: birth_date = date_of_birth # 仮ユーザー登録 register_data = { "email": email, "password": password, "firstname": firstname, "lastname": lastname, "date_of_birth": birth_date, "female": False, # デフォルト値 "is_rogaining": True } response = self.session.post(f"{self.base_url}/register/", json=register_data) if response.status_code in [200, 201]: logger.info(f"仮ユーザー登録成功: {email}") # 実際のシステムでは、メール認証コードを使って本登録を完了する必要があります # ここでは簡略化のため、直接ログインを試行します time.sleep(1) # 少し待機 login_data = { "identifier": email, "password": password } login_response = self.session.post(f"{self.base_url}/login/", json=login_data) if login_response.status_code == 200: result = login_response.json() token = result.get('token') user_id = result.get('user', {}).get('id') logger.info(f"新規ユーザーのログイン成功: {email}") self.stats['users_created'] += 1 return True, str(user_id), token else: logger.warning(f"新規ユーザーのログインに失敗: {email}") # メール認証が必要な可能性があります self.stats['users_created'] += 1 return True, "pending_verification", None else: error_msg = response.text logger.error(f"ユーザー登録失敗: {email} - {error_msg}") return False, None, None except Exception as e: logger.error(f"新規ユーザー作成エラー: {str(e)}") return False, None, None def register_team_and_members(self, team_data: Dict, zekken_number: int) -> Tuple[bool, Optional[str]]: """ チーム登録とメンバー登録 Args: team_data: チームデータ(CSVの1行分) zekken_number: ゼッケン番号 Returns: Tuple[success, team_id] """ if self.dry_run: logger.info(f"[DRY RUN] Would register team: {team_data['チーム名']} with zekken: {zekken_number}") return True, "dummy_team_id" try: # チーム登録データを準備 register_data = { "zekken_number": zekken_number, "event_code": self.event_code, "team_name": team_data['チーム名'], "class_name": team_data['部門'], "password": team_data['パスワード'] } # チーム登録API呼び出し response = self.session.post(f"{self.base_url}/register_team", json=register_data) if response.status_code in [200, 201]: result = response.json() if result.get('status') == 'OK': team_id = result.get('team_id') logger.info(f"チーム登録成功: {team_data['チーム名']} (zekken: {zekken_number})") self.stats['teams_registered'] += 1 # メンバー登録 success = self._register_team_members(team_data, team_id) return success, str(team_id) else: logger.error(f"チーム登録エラー: {result.get('message')}") return False, None else: logger.error(f"チーム登録API呼び出し失敗: {response.status_code} - {response.text}") return False, None except Exception as e: logger.error(f"チーム登録エラー: {str(e)}") return False, None def _register_team_members(self, team_data: Dict, team_id: str) -> bool: """ チームメンバーを登録(最大7名) Args: team_data: チームデータ team_id: チームID Returns: 成功フラグ """ if self.dry_run: logger.info(f"[DRY RUN] Would register team members for team: {team_id}") return True try: success_count = 0 # メンバー1-7を順番に処理 for i in range(1, 8): name_key = f'氏名{i}' birth_key = f'誕生日{i}' if name_key in team_data and team_data[name_key].strip(): name = team_data[name_key].strip() birth_date = team_data.get(birth_key, '1990/01/01') # ダミーメールアドレスを生成 dummy_email = f"{name.replace(' ', '')}_{team_id}_{i}@dummy.local" # メンバー追加データ member_data = { "email": dummy_email, "firstname": name.split()[0] if ' ' in name else name, "lastname": name.split()[-1] if ' ' in name else "", "date_of_birth": birth_date.replace('/', '-'), "female": False # デフォルト値 } # メンバー追加API呼び出し response = self.session.post( f"{self.base_url}/teams/{team_id}/members/", json=member_data ) if response.status_code in [200, 201]: logger.info(f"メンバー追加成功: {name} -> チーム{team_id}") success_count += 1 else: logger.warning(f"メンバー追加失敗: {name} - {response.text}") logger.info(f"チーム{team_id}のメンバー登録完了: {success_count}名") return success_count > 0 except Exception as e: logger.error(f"メンバー登録エラー: {str(e)}") return False def create_event_entry(self, team_id: str, category_id: int = 1) -> Tuple[bool, Optional[str]]: """ イベントエントリー登録 Args: team_id: チームID category_id: カテゴリID Returns: Tuple[success, entry_id] """ if self.dry_run: logger.info(f"[DRY RUN] Would create event entry for team: {team_id}") return True, "dummy_entry_id" try: # エントリーデータ準備 entry_data = { "team_id": team_id, "event_code": self.event_code, "category": category_id, "entry_date": datetime.now().strftime("%Y-%m-%d") } # エントリー登録API呼び出し response = self.session.post(f"{self.base_url}/entry/", json=entry_data) if response.status_code in [200, 201]: result = response.json() entry_id = result.get('id') or result.get('entry_id') logger.info(f"エントリー登録成功: team_id={team_id}, entry_id={entry_id}") self.stats['entries_created'] += 1 return True, str(entry_id) else: logger.error(f"エントリー登録失敗: {response.status_code} - {response.text}") return False, None except Exception as e: logger.error(f"エントリー登録エラー: {str(e)}") return False, None def participate_in_event(self, entry_id: str, zekken_number: int) -> bool: """ イベント参加処理 Args: entry_id: エントリーID zekken_number: ゼッケン番号 Returns: 成功フラグ """ if self.dry_run: logger.info(f"[DRY RUN] Would participate in event: entry_id={entry_id}, zekken={zekken_number}") return True try: # イベント参加データ準備 participation_data = { "entry_id": entry_id, "event_code": self.event_code, "zekken_number": zekken_number, "participation_date": datetime.now().strftime("%Y-%m-%d") } # イベント参加API呼び出し(実際のAPIエンドポイントに合わせて調整が必要) response = self.session.post(f"{self.base_url}/start_from_rogapp", json=participation_data) if response.status_code in [200, 201]: logger.info(f"イベント参加成功: entry_id={entry_id}, zekken={zekken_number}") self.stats['participations_created'] += 1 return True else: logger.warning(f"イベント参加API呼び出し結果: {response.status_code} - {response.text}") # 参加処理は必須ではないため、警告のみでTrueを返す return True except Exception as e: logger.error(f"イベント参加エラー: {str(e)}") return True # 参加処理は必須ではないため、エラーでもTrueを返す def process_csv_file(self, csv_file_path: str) -> bool: """ CSVファイルを処理してユーザー登録からイベント参加まで実行 Args: csv_file_path: CSVファイルパス Returns: 成功フラグ """ try: if not os.path.exists(csv_file_path): logger.error(f"CSVファイルが見つかりません: {csv_file_path}") return False with open(csv_file_path, 'r', encoding='utf-8') as file: csv_reader = csv.DictReader(file) for row_num, row in enumerate(csv_reader, start=1): try: self._process_team_row(row, row_num) # API呼び出し間隔を空ける if not self.dry_run: time.sleep(0.5) except Exception as e: error_msg = f"行{row_num}の処理でエラー: {str(e)}" logger.error(error_msg) self.stats['errors'].append(error_msg) continue return True except Exception as e: logger.error(f"CSVファイル処理エラー: {str(e)}") return False def _process_team_row(self, row: Dict, row_num: int): """ CSVの1行(1チーム)を処理 Args: row: CSV行データ row_num: 行番号 """ team_name = row.get('チーム名', '').strip() email = row.get('メール', '').strip() password = row.get('password', '').strip() phone = row.get('電話番号', '').strip() if not all([team_name, email, password]): logger.warning(f"行{row_num}: 必須項目が不足 - チーム名={team_name}, メール={email}") return logger.info(f"行{row_num}の処理開始: チーム={team_name}") # ゼッケン番号を生成(行番号ベース、実際の運用では別途管理が必要) zekken_number = row_num # 2-1. カスタムユーザー登録 # 最初のメンバー(氏名1)をメインユーザーとして使用 firstname = row.get('氏名1', team_name).strip() lastname = "" if ' ' in firstname: parts = firstname.split(' ', 1) firstname = parts[0] lastname = parts[1] date_of_birth = row.get('誕生日1', '1990/01/01') user_success, user_id, token = self.get_or_create_user( email, password, firstname, lastname, date_of_birth, phone ) if not user_success: logger.error(f"行{row_num}: ユーザー登録/取得失敗") return # 2-2. チーム登録、メンバー登録 team_success, team_id = self.register_team_and_members(row, zekken_number) if not team_success: logger.error(f"行{row_num}: チーム登録失敗") return # 2-3. エントリー登録 entry_success, entry_id = self.create_event_entry(team_id) if not entry_success: logger.error(f"行{row_num}: エントリー登録失敗") return # 2-4. イベント参加 participation_success = self.participate_in_event(entry_id, zekken_number) if participation_success: logger.info(f"行{row_num}: 全処理完了 - チーム={team_name}, zekken={zekken_number}") self.stats['processed_teams'] += 1 else: logger.warning(f"行{row_num}: イベント参加処理で警告") def print_statistics(self): """ 処理統計を出力 """ logger.info("=== 処理統計 ===") logger.info(f"処理完了チーム数: {self.stats['processed_teams']}") logger.info(f"作成ユーザー数: {self.stats['users_created']}") logger.info(f"更新ユーザー数: {self.stats['users_updated']}") logger.info(f"登録チーム数: {self.stats['teams_registered']}") logger.info(f"作成エントリー数: {self.stats['entries_created']}") logger.info(f"参加登録数: {self.stats['participations_created']}") logger.info(f"エラー数: {len(self.stats['errors'])}") if self.stats['errors']: logger.error("エラー詳細:") for error in self.stats['errors']: logger.error(f" - {error}") def main(): parser = argparse.ArgumentParser(description='イベントユーザー登録スクリプト') parser.add_argument('--event_code', required=True, help='イベントコード(例: 大垣2509)') parser.add_argument('--csv_file', default='CPLIST/input/team2025.csv', help='CSVファイルパス') parser.add_argument('--base_url', default='http://localhost:8000', help='APIベースURL') parser.add_argument('--dry_run', action='store_true', help='テスト実行(実際のAPI呼び出しなし)') args = parser.parse_args() logger.info(f"イベントユーザー登録処理開始: event_code={args.event_code}") # 登録処理実行 registration = EventUserRegistration( event_code=args.event_code, base_url=args.base_url, dry_run=args.dry_run ) success = registration.process_csv_file(args.csv_file) # 統計出力 registration.print_statistics() if success: logger.info("処理が正常に完了しました") return 0 else: logger.error("処理中にエラーが発生しました") return 1 if __name__ == "__main__": sys.exit(main())