commit 7df12119fbeda257e23196411ae8d8645e98bbc4 Author: arthur Date: Wed Aug 20 14:03:31 2025 +0700 first push diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..34ac6ea Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5480842 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "kiroAgent.configureMCP": "Disabled" +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..97532ec --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,59 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Python data comparison tool built with Python 3.13+. The project is currently in early development with a minimal structure containing: + +- A basic Python application entry point (`main.py`) +- Sample data in Excel format (`data/sample-data.xlsx`) +- Standard Python packaging configuration (`pyproject.toml`) + +## Development Commands + +### Running the Application +```bash +uv run main.py +``` +This launches a web-based GUI at http://localhost:8080 + +### Running Analysis Only (Command Line) +```bash +uv run data_comparator.py +``` + +### Project Setup +The project uses Python 3.13+ with uv for dependency management. Dependencies include: +- pandas (Excel file processing) +- openpyxl (Excel file reading) +- flask (Web GUI) + +## Project Structure + +- `main.py` - Main application entry point that launches the web GUI +- `data_comparator.py` - Core comparison logic for KST vs Coordi data analysis +- `web_gui.py` - Flask-based web GUI application +- `analyze_excel.py` - Basic Excel file structure analysis utility +- `data/` - Directory containing sample data files + - `sample-data.xlsx` - Sample Excel data file for comparison operations +- `templates/` - HTML templates for web GUI (auto-generated) +- `pyproject.toml` - Python project configuration and metadata + +## Key Features + +- **KST vs Coordi Comparison**: Compares data between KST columns (`Title KR`, `Epi.`) and Coordi columns (`KR title`, `Chap`) +- **Mismatch Categorization**: Identifies KST-only, Coordi-only, and duplicate items +- **Data Reconciliation**: Ensures matching counts after excluding mismatches +- **Web-based GUI**: Interactive interface with tabs for different data views +- **File Upload**: Upload Excel files directly through the web interface +- **Sheet Filtering**: Filter results by specific Excel sheets +- **Real-time Analysis**: Live comparison with detailed mismatch reasons + +## Comparison Logic + +The tool compares Excel data by: +1. Finding columns by header names (not positions) +2. Extracting title+episode combinations from both datasets +3. Categorizing mismatches and calculating reconciliation +4. Displaying results with reasons for each discrepancy \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/analyze_excel.py b/analyze_excel.py new file mode 100644 index 0000000..4f6887b --- /dev/null +++ b/analyze_excel.py @@ -0,0 +1,92 @@ +import pandas as pd +import os +from pathlib import Path + +def analyze_excel_structure(file_path): + """ + Analyze the structure of an Excel file and return detailed information. + """ + if not os.path.exists(file_path): + return f"Error: File {file_path} does not exist" + + try: + # Read Excel file to get sheet names + excel_file = pd.ExcelFile(file_path) + + analysis = { + 'file_path': file_path, + 'file_size': os.path.getsize(file_path), + 'sheet_names': excel_file.sheet_names, + 'total_sheets': len(excel_file.sheet_names), + 'sheets_analysis': {} + } + + # Analyze each sheet + for sheet_name in excel_file.sheet_names: + df = pd.read_excel(file_path, sheet_name=sheet_name) + + sheet_info = { + 'dimensions': df.shape, + 'columns': list(df.columns), + 'column_count': len(df.columns), + 'row_count': len(df), + 'data_types': df.dtypes.to_dict(), + 'missing_values': df.isnull().sum().to_dict(), + 'sample_data': df.head(3).to_dict('records') if not df.empty else [] + } + + analysis['sheets_analysis'][sheet_name] = sheet_info + + return analysis + + except Exception as e: + return f"Error analyzing file: {str(e)}" + +def print_analysis_report(analysis): + """ + Print a formatted report of the Excel file analysis. + """ + if isinstance(analysis, str): + print(analysis) + return + + print("=" * 60) + print("EXCEL FILE STRUCTURE ANALYSIS") + print("=" * 60) + + print(f"File: {analysis['file_path']}") + print(f"Size: {analysis['file_size']:,} bytes") + print(f"Total Sheets: {analysis['total_sheets']}") + print(f"Sheet Names: {', '.join(analysis['sheet_names'])}") + print() + + for sheet_name, sheet_info in analysis['sheets_analysis'].items(): + print(f"--- SHEET: {sheet_name} ---") + print(f"Dimensions: {sheet_info['dimensions'][0]} rows × {sheet_info['dimensions'][1]} columns") + print(f"Columns: {', '.join(sheet_info['columns'])}") + print() + + print("Data Types:") + for col, dtype in sheet_info['data_types'].items(): + print(f" {col}: {dtype}") + print() + + print("Missing Values:") + for col, missing in sheet_info['missing_values'].items(): + if missing > 0: + print(f" {col}: {missing} missing values") + print() + + if sheet_info['sample_data']: + print("Sample Data (first 3 rows):") + for i, row in enumerate(sheet_info['sample_data'], 1): + print(f" Row {i}: {row}") + print() + +if __name__ == "__main__": + # Analyze the sample Excel file + file_path = "data/sample-data.xlsx" + + print("Starting Excel file analysis...") + analysis = analyze_excel_structure(file_path) + print_analysis_report(analysis) \ No newline at end of file diff --git a/data/sample-data.xlsx b/data/sample-data.xlsx new file mode 100644 index 0000000..720976a Binary files /dev/null and b/data/sample-data.xlsx differ diff --git a/data/~$sample-data.xlsx b/data/~$sample-data.xlsx new file mode 100644 index 0000000..7719d1a Binary files /dev/null and b/data/~$sample-data.xlsx differ diff --git a/data_comparator.py b/data_comparator.py new file mode 100644 index 0000000..06cfd23 --- /dev/null +++ b/data_comparator.py @@ -0,0 +1,488 @@ +import pandas as pd +import numpy as np +from typing import Dict, List, Tuple, Any, Set +from dataclasses import dataclass + +@dataclass +class ComparisonItem: + """Represents a single item for comparison""" + title: str + episode: str + source_sheet: str + row_index: int + + def __hash__(self): + return hash((self.title, self.episode)) + + def __eq__(self, other): + if not isinstance(other, ComparisonItem): + return False + return self.title == other.title and self.episode == other.episode + +class KSTCoordiComparator: + """ + Compare KST and Coordi data to identify mismatches and ensure count reconciliation + """ + + def __init__(self, excel_file_path: str): + self.excel_file_path = excel_file_path + self.data = {} + self.kst_items = set() + self.coordi_items = set() + self.comparison_results = {} + + def load_data(self) -> bool: + """Load data from Excel file""" + try: + excel_file = pd.ExcelFile(self.excel_file_path) + for sheet_name in excel_file.sheet_names: + self.data[sheet_name] = pd.read_excel(self.excel_file_path, sheet_name=sheet_name) + return True + except Exception as e: + print(f"Error loading data: {e}") + return False + + def extract_kst_coordi_items(self) -> Dict[str, Any]: + """Extract KST and Coordi items from all sheets using column header names""" + kst_items = set() + coordi_items = set() + kst_details = [] + coordi_details = [] + + for sheet_name, df in self.data.items(): + columns = df.columns.tolist() + + # Find columns by header names + # KST columns: 'Title KR' and 'Epi.' + # Coordi columns: 'KR title' and 'Chap' + + kst_title_col = None + kst_episode_col = None + coordi_title_col = None + coordi_episode_col = None + + # Find KST columns + for col in columns: + if col == 'Title KR': + kst_title_col = col + elif col == 'Epi.': + kst_episode_col = col + + # Find Coordi columns + for col in columns: + if col == 'KR title': + coordi_title_col = col + elif col == 'Chap': + coordi_episode_col = col + + print(f"Sheet: {sheet_name}") + print(f" KST columns - Title: {kst_title_col}, Episode: {kst_episode_col}") + print(f" Coordi columns - Title: {coordi_title_col}, Episode: {coordi_episode_col}") + + # Extract items from each row + for idx, row in df.iterrows(): + # Extract KST data + if kst_title_col and kst_episode_col: + kst_title = str(row.get(kst_title_col, '')).strip() + kst_episode = str(row.get(kst_episode_col, '')).strip() + + # Check if this row has valid KST data + has_kst_data = ( + kst_title and kst_title != 'nan' and + kst_episode and kst_episode != 'nan' and + pd.notna(row[kst_title_col]) and pd.notna(row[kst_episode_col]) + ) + + if has_kst_data: + item = ComparisonItem(kst_title, kst_episode, sheet_name, idx) + kst_items.add(item) + kst_details.append({ + 'title': kst_title, + 'episode': kst_episode, + 'sheet': sheet_name, + 'row_index': idx, + 'kst_data': { + kst_title_col: row[kst_title_col], + kst_episode_col: row[kst_episode_col] + } + }) + + # Extract Coordi data + if coordi_title_col and coordi_episode_col: + coordi_title = str(row.get(coordi_title_col, '')).strip() + coordi_episode = str(row.get(coordi_episode_col, '')).strip() + + # Check if this row has valid Coordi data + has_coordi_data = ( + coordi_title and coordi_title != 'nan' and + coordi_episode and coordi_episode != 'nan' and + pd.notna(row[coordi_title_col]) and pd.notna(row[coordi_episode_col]) + ) + + if has_coordi_data: + item = ComparisonItem(coordi_title, coordi_episode, sheet_name, idx) + coordi_items.add(item) + coordi_details.append({ + 'title': coordi_title, + 'episode': coordi_episode, + 'sheet': sheet_name, + 'row_index': idx, + 'coordi_data': { + coordi_title_col: row[coordi_title_col], + coordi_episode_col: row[coordi_episode_col] + } + }) + + self.kst_items = kst_items + self.coordi_items = coordi_items + + return { + 'kst_items': kst_items, + 'coordi_items': coordi_items, + 'kst_details': kst_details, + 'coordi_details': coordi_details + } + + def categorize_mismatches(self) -> Dict[str, Any]: + """Categorize data into KST-only, Coordi-only, and matched items""" + if not self.kst_items or not self.coordi_items: + self.extract_kst_coordi_items() + + # Find overlaps and differences + matched_items = self.kst_items.intersection(self.coordi_items) + kst_only_items = self.kst_items - self.coordi_items + coordi_only_items = self.coordi_items - self.kst_items + + # Find duplicates within each dataset + kst_duplicates = self._find_duplicates_in_set(self.kst_items) + coordi_duplicates = self._find_duplicates_in_set(self.coordi_items) + + categorization = { + 'matched_items': list(matched_items), + 'kst_only_items': list(kst_only_items), + 'coordi_only_items': list(coordi_only_items), + 'kst_duplicates': kst_duplicates, + 'coordi_duplicates': coordi_duplicates, + 'counts': { + 'total_kst': len(self.kst_items), + 'total_coordi': len(self.coordi_items), + 'matched': len(matched_items), + 'kst_only': len(kst_only_items), + 'coordi_only': len(coordi_only_items), + 'kst_duplicates_count': len(kst_duplicates), + 'coordi_duplicates_count': len(coordi_duplicates) + } + } + + # Calculate reconciled counts (after removing mismatches) + reconciled_kst_count = len(matched_items) + reconciled_coordi_count = len(matched_items) + + categorization['reconciliation'] = { + 'original_kst_count': len(self.kst_items), + 'original_coordi_count': len(self.coordi_items), + 'reconciled_kst_count': reconciled_kst_count, + 'reconciled_coordi_count': reconciled_coordi_count, + 'counts_match_after_reconciliation': reconciled_kst_count == reconciled_coordi_count, + 'items_to_exclude_from_kst': len(kst_only_items) + len(kst_duplicates), + 'items_to_exclude_from_coordi': len(coordi_only_items) + len(coordi_duplicates) + } + + return categorization + + def _find_duplicates_in_set(self, items_set: Set[ComparisonItem]) -> List[ComparisonItem]: + """Find duplicate items within a dataset""" + # Convert to list to check for duplicates + items_list = list(items_set) + seen = set() + duplicates = [] + + for item in items_list: + key = (item.title, item.episode) + if key in seen: + duplicates.append(item) + else: + seen.add(key) + + return duplicates + + def generate_mismatch_details(self) -> Dict[str, List[Dict]]: + """Generate detailed information about each type of mismatch with reasons""" + categorization = self.categorize_mismatches() + + mismatch_details = { + 'kst_only': [], + 'coordi_only': [], + 'kst_duplicates': [], + 'coordi_duplicates': [] + } + + # KST-only items + for item in categorization['kst_only_items']: + mismatch_details['kst_only'].append({ + 'title': item.title, + 'episode': item.episode, + 'sheet': item.source_sheet, + 'row_index': item.row_index, + 'reason': 'Item exists in KST data but not in Coordi data', + 'mismatch_type': 'KST_ONLY' + }) + + # Coordi-only items + for item in categorization['coordi_only_items']: + mismatch_details['coordi_only'].append({ + 'title': item.title, + 'episode': item.episode, + 'sheet': item.source_sheet, + 'row_index': item.row_index, + 'reason': 'Item exists in Coordi data but not in KST data', + 'mismatch_type': 'COORDI_ONLY' + }) + + # KST duplicates + for item in categorization['kst_duplicates']: + mismatch_details['kst_duplicates'].append({ + 'title': item.title, + 'episode': item.episode, + 'sheet': item.source_sheet, + 'row_index': item.row_index, + 'reason': 'Duplicate entry in KST data', + 'mismatch_type': 'KST_DUPLICATE' + }) + + # Coordi duplicates + for item in categorization['coordi_duplicates']: + mismatch_details['coordi_duplicates'].append({ + 'title': item.title, + 'episode': item.episode, + 'sheet': item.source_sheet, + 'row_index': item.row_index, + 'reason': 'Duplicate entry in Coordi data', + 'mismatch_type': 'COORDI_DUPLICATE' + }) + + return mismatch_details + + def get_comparison_summary(self, sheet_filter: str = None) -> Dict[str, Any]: + """Get a comprehensive summary of the comparison, optionally filtered by sheet""" + categorization = self.categorize_mismatches() + mismatch_details = self.generate_mismatch_details() + grouped_data = self.group_by_title() + + # Get sheet names for filtering options + sheet_names = list(self.data.keys()) if self.data else [] + + # Apply sheet filtering if specified + if sheet_filter and sheet_filter != 'All Sheets': + mismatch_details = self.filter_by_sheet(mismatch_details, sheet_filter) + grouped_data = self.filter_grouped_data_by_sheet(grouped_data, sheet_filter) + + # Recalculate counts for filtered data + filtered_counts = self.calculate_filtered_counts(mismatch_details) + else: + filtered_counts = { + 'kst_total': categorization['counts']['total_kst'], + 'coordi_total': categorization['counts']['total_coordi'], + 'matched': categorization['counts']['matched'], + 'kst_only_count': categorization['counts']['kst_only'], + 'coordi_only_count': categorization['counts']['coordi_only'], + 'kst_duplicates_count': categorization['counts']['kst_duplicates_count'], + 'coordi_duplicates_count': categorization['counts']['coordi_duplicates_count'] + } + + summary = { + 'sheet_names': sheet_names, + 'current_sheet_filter': sheet_filter or 'All Sheets', + 'original_counts': { + 'kst_total': filtered_counts['kst_total'], + 'coordi_total': filtered_counts['coordi_total'] + }, + 'matched_items_count': filtered_counts['matched'], + 'mismatches': { + 'kst_only_count': filtered_counts['kst_only_count'], + 'coordi_only_count': filtered_counts['coordi_only_count'], + 'kst_duplicates_count': filtered_counts['kst_duplicates_count'], + 'coordi_duplicates_count': filtered_counts['coordi_duplicates_count'] + }, + 'reconciliation': categorization['reconciliation'], + 'mismatch_details': mismatch_details, + 'grouped_by_title': grouped_data + } + + return summary + + def filter_by_sheet(self, mismatch_details: Dict[str, List], sheet_filter: str) -> Dict[str, List]: + """Filter mismatch details by specific sheet""" + filtered = {} + for category, items in mismatch_details.items(): + filtered[category] = [item for item in items if item.get('sheet') == sheet_filter] + return filtered + + def filter_grouped_data_by_sheet(self, grouped_data: Dict, sheet_filter: str) -> Dict: + """Filter grouped data by specific sheet""" + filtered = { + 'kst_only_by_title': {}, + 'coordi_only_by_title': {}, + 'matched_by_title': {}, + 'title_summaries': {} + } + + # Filter each category + for category in ['kst_only_by_title', 'coordi_only_by_title', 'matched_by_title']: + for title, items in grouped_data[category].items(): + filtered_items = [item for item in items if item.get('sheet') == sheet_filter] + if filtered_items: + filtered[category][title] = filtered_items + + # Recalculate title summaries for filtered data + all_titles = set() + all_titles.update(filtered['kst_only_by_title'].keys()) + all_titles.update(filtered['coordi_only_by_title'].keys()) + all_titles.update(filtered['matched_by_title'].keys()) + + for title in all_titles: + kst_only_count = len(filtered['kst_only_by_title'].get(title, [])) + coordi_only_count = len(filtered['coordi_only_by_title'].get(title, [])) + matched_count = len(filtered['matched_by_title'].get(title, [])) + total_episodes = kst_only_count + coordi_only_count + matched_count + + filtered['title_summaries'][title] = { + 'total_episodes': total_episodes, + 'matched_count': matched_count, + 'kst_only_count': kst_only_count, + 'coordi_only_count': coordi_only_count, + 'match_percentage': round((matched_count / total_episodes * 100) if total_episodes > 0 else 0, 1), + 'has_mismatches': kst_only_count > 0 or coordi_only_count > 0 + } + + return filtered + + def calculate_filtered_counts(self, filtered_mismatch_details: Dict[str, List]) -> Dict[str, int]: + """Calculate counts for filtered data""" + return { + 'kst_total': len(filtered_mismatch_details['kst_only']) + len(filtered_mismatch_details['kst_duplicates']), + 'coordi_total': len(filtered_mismatch_details['coordi_only']) + len(filtered_mismatch_details['coordi_duplicates']), + 'matched': 0, # Will be calculated from matched data separately + 'kst_only_count': len(filtered_mismatch_details['kst_only']), + 'coordi_only_count': len(filtered_mismatch_details['coordi_only']), + 'kst_duplicates_count': len(filtered_mismatch_details['kst_duplicates']), + 'coordi_duplicates_count': len(filtered_mismatch_details['coordi_duplicates']) + } + + def group_by_title(self) -> Dict[str, Any]: + """Group mismatches and matches by KR title""" + from collections import defaultdict + + grouped = { + 'kst_only_by_title': defaultdict(list), + 'coordi_only_by_title': defaultdict(list), + 'matched_by_title': defaultdict(list), + 'title_summaries': {} + } + + # Get mismatch details + mismatch_details = self.generate_mismatch_details() + + # Group KST only items by title + for item in mismatch_details['kst_only']: + title = item['title'] + grouped['kst_only_by_title'][title].append(item) + + # Group Coordi only items by title + for item in mismatch_details['coordi_only']: + title = item['title'] + grouped['coordi_only_by_title'][title].append(item) + + # Group matched items by title + if hasattr(self, 'kst_items') and hasattr(self, 'coordi_items'): + categorization = self.categorize_mismatches() + matched_items = categorization['matched_items'] + + for item in matched_items: + title = item.title + grouped['matched_by_title'][title].append({ + 'title': item.title, + 'episode': item.episode, + 'sheet': item.source_sheet, + 'row_index': item.row_index, + 'reason': 'Perfect match' + }) + + # Create summary for each title + all_titles = set() + all_titles.update(grouped['kst_only_by_title'].keys()) + all_titles.update(grouped['coordi_only_by_title'].keys()) + all_titles.update(grouped['matched_by_title'].keys()) + + for title in all_titles: + kst_only_count = len(grouped['kst_only_by_title'][title]) + coordi_only_count = len(grouped['coordi_only_by_title'][title]) + matched_count = len(grouped['matched_by_title'][title]) + total_episodes = kst_only_count + coordi_only_count + matched_count + + grouped['title_summaries'][title] = { + 'total_episodes': total_episodes, + 'matched_count': matched_count, + 'kst_only_count': kst_only_count, + 'coordi_only_count': coordi_only_count, + 'match_percentage': round((matched_count / total_episodes * 100) if total_episodes > 0 else 0, 1), + 'has_mismatches': kst_only_count > 0 or coordi_only_count > 0 + } + + # Convert defaultdicts to regular dicts for JSON serialization + grouped['kst_only_by_title'] = dict(grouped['kst_only_by_title']) + grouped['coordi_only_by_title'] = dict(grouped['coordi_only_by_title']) + grouped['matched_by_title'] = dict(grouped['matched_by_title']) + + return grouped + + def print_comparison_summary(self): + """Print a formatted summary of the comparison""" + summary = self.get_comparison_summary() + + print("=" * 80) + print("KST vs COORDI COMPARISON SUMMARY") + print("=" * 80) + + print(f"Original Counts:") + print(f" KST Total: {summary['original_counts']['kst_total']}") + print(f" Coordi Total: {summary['original_counts']['coordi_total']}") + print() + + print(f"Matched Items: {summary['matched_items_count']}") + print() + + print(f"Mismatches:") + print(f" KST Only: {summary['mismatches']['kst_only_count']}") + print(f" Coordi Only: {summary['mismatches']['coordi_only_count']}") + print(f" KST Duplicates: {summary['mismatches']['kst_duplicates_count']}") + print(f" Coordi Duplicates: {summary['mismatches']['coordi_duplicates_count']}") + print() + + print(f"Reconciliation:") + reconciliation = summary['reconciliation'] + print(f" After excluding mismatches:") + print(f" KST Count: {reconciliation['reconciled_kst_count']}") + print(f" Coordi Count: {reconciliation['reconciled_coordi_count']}") + print(f" Counts Match: {reconciliation['counts_match_after_reconciliation']}") + print() + + # Show sample mismatches + for mismatch_type, details in summary['mismatch_details'].items(): + if details: + print(f"{mismatch_type.upper()} (showing first 3):") + for i, item in enumerate(details[:3]): + print(f" {i+1}. {item['title']} - Episode {item['episode']} ({item['reason']})") + if len(details) > 3: + print(f" ... and {len(details) - 3} more") + print() + +if __name__ == "__main__": + # Test the comparator + comparator = KSTCoordiComparator("data/sample-data.xlsx") + + if comparator.load_data(): + print("Data loaded successfully!") + comparator.print_comparison_summary() + else: + print("Failed to load data!") \ No newline at end of file diff --git a/gui_app.py b/gui_app.py new file mode 100644 index 0000000..b3dd952 --- /dev/null +++ b/gui_app.py @@ -0,0 +1,319 @@ +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +import pandas as pd +from pathlib import Path +from data_comparator import KSTCoordiComparator + +class DataComparisonGUI: + def __init__(self, root): + self.root = root + self.root.title("KST vs Coordi Data Comparison Tool") + self.root.geometry("1200x800") + + self.comparator = None + self.comparison_data = None + + self.setup_ui() + + def setup_ui(self): + # Main container + main_frame = ttk.Frame(self.root, padding="10") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # Configure grid weights + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + main_frame.columnconfigure(1, weight=1) + main_frame.rowconfigure(2, weight=1) + + # Title + title_label = ttk.Label(main_frame, text="KST vs Coordi Data Comparison", + font=("Arial", 16, "bold")) + title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20)) + + # File selection frame + file_frame = ttk.LabelFrame(main_frame, text="File Selection", padding="10") + file_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10)) + file_frame.columnconfigure(1, weight=1) + + ttk.Label(file_frame, text="Excel File:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10)) + + self.file_path_var = tk.StringVar(value="data/sample-data.xlsx") + self.file_entry = ttk.Entry(file_frame, textvariable=self.file_path_var, width=50) + self.file_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10)) + + browse_btn = ttk.Button(file_frame, text="Browse", command=self.browse_file) + browse_btn.grid(row=0, column=2) + + analyze_btn = ttk.Button(file_frame, text="Analyze Data", command=self.analyze_data) + analyze_btn.grid(row=0, column=3, padx=(10, 0)) + + # Results notebook (tabs) + self.notebook = ttk.Notebook(main_frame) + self.notebook.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # Create tabs + self.create_summary_tab() + self.create_matched_tab() + self.create_kst_only_tab() + self.create_coordi_only_tab() + + # Status bar + self.status_var = tk.StringVar(value="Ready - Select an Excel file and click 'Analyze Data'") + status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN) + status_bar.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(10, 0)) + + def create_summary_tab(self): + # Summary tab + summary_frame = ttk.Frame(self.notebook) + self.notebook.add(summary_frame, text="Summary") + + # Configure grid + summary_frame.columnconfigure(0, weight=1) + summary_frame.rowconfigure(1, weight=1) + + # Summary text widget + summary_text_frame = ttk.LabelFrame(summary_frame, text="Comparison Summary", padding="10") + summary_text_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=10, pady=10) + summary_text_frame.columnconfigure(0, weight=1) + summary_text_frame.rowconfigure(0, weight=1) + + self.summary_text = tk.Text(summary_text_frame, wrap=tk.WORD, height=15) + summary_scrollbar = ttk.Scrollbar(summary_text_frame, orient=tk.VERTICAL, command=self.summary_text.yview) + self.summary_text.configure(yscrollcommand=summary_scrollbar.set) + + self.summary_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + summary_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) + + # Reconciliation info + reconcile_frame = ttk.LabelFrame(summary_frame, text="Reconciliation Results", padding="10") + reconcile_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), padx=10, pady=(0, 10)) + + self.reconcile_text = tk.Text(reconcile_frame, wrap=tk.WORD, height=8) + reconcile_scrollbar = ttk.Scrollbar(reconcile_frame, orient=tk.VERTICAL, command=self.reconcile_text.yview) + self.reconcile_text.configure(yscrollcommand=reconcile_scrollbar.set) + + self.reconcile_text.grid(row=0, column=0, sticky=(tk.W, tk.E)) + reconcile_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) + + reconcile_frame.columnconfigure(0, weight=1) + + def create_matched_tab(self): + matched_frame = ttk.Frame(self.notebook) + self.notebook.add(matched_frame, text="Matched Items") + + self.create_data_table(matched_frame, "matched") + + def create_kst_only_tab(self): + kst_frame = ttk.Frame(self.notebook) + self.notebook.add(kst_frame, text="KST Only") + + self.create_data_table(kst_frame, "kst_only") + + def create_coordi_only_tab(self): + coordi_frame = ttk.Frame(self.notebook) + self.notebook.add(coordi_frame, text="Coordi Only") + + self.create_data_table(coordi_frame, "coordi_only") + + def create_data_table(self, parent, table_type): + # Configure grid + parent.columnconfigure(0, weight=1) + parent.rowconfigure(1, weight=1) + + # Info label + info_frame = ttk.Frame(parent) + info_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=10, pady=10) + info_frame.columnconfigure(1, weight=1) + + count_label = ttk.Label(info_frame, text="Count:") + count_label.grid(row=0, column=0, padx=(0, 10)) + + count_var = tk.StringVar(value="0") + setattr(self, f"{table_type}_count_var", count_var) + count_display = ttk.Label(info_frame, textvariable=count_var, font=("Arial", 10, "bold")) + count_display.grid(row=0, column=1, sticky=tk.W) + + # Table frame + table_frame = ttk.Frame(parent) + table_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=10, pady=(0, 10)) + table_frame.columnconfigure(0, weight=1) + table_frame.rowconfigure(0, weight=1) + + # Create treeview + columns = ("Title", "Episode", "Sheet", "Row", "Reason") + tree = ttk.Treeview(table_frame, columns=columns, show="headings", height=20) + + # Configure columns + tree.heading("Title", text="Title") + tree.heading("Episode", text="Episode") + tree.heading("Sheet", text="Sheet") + tree.heading("Row", text="Row") + tree.heading("Reason", text="Reason") + + tree.column("Title", width=300) + tree.column("Episode", width=100) + tree.column("Sheet", width=120) + tree.column("Row", width=80) + tree.column("Reason", width=300) + + # Scrollbars + v_scrollbar = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=tree.yview) + h_scrollbar = ttk.Scrollbar(table_frame, orient=tk.HORIZONTAL, command=tree.xview) + tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) + + tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + v_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) + h_scrollbar.grid(row=1, column=0, sticky=(tk.W, tk.E)) + + # Store tree widget + setattr(self, f"{table_type}_tree", tree) + + def browse_file(self): + file_path = filedialog.askopenfilename( + title="Select Excel File", + filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")] + ) + if file_path: + self.file_path_var.set(file_path) + + def analyze_data(self): + file_path = self.file_path_var.get().strip() + + if not file_path: + messagebox.showerror("Error", "Please select an Excel file") + return + + if not Path(file_path).exists(): + messagebox.showerror("Error", f"File not found: {file_path}") + return + + try: + self.status_var.set("Analyzing data...") + self.root.update() + + # Create comparator and analyze + self.comparator = KSTCoordiComparator(file_path) + if not self.comparator.load_data(): + messagebox.showerror("Error", "Failed to load Excel data") + return + + # Get comparison results + self.comparison_data = self.comparator.get_comparison_summary() + + # Update GUI + self.update_summary() + self.update_data_tables() + + self.status_var.set("Analysis complete!") + + except Exception as e: + messagebox.showerror("Error", f"Analysis failed: {str(e)}") + self.status_var.set("Analysis failed") + + def update_summary(self): + if not self.comparison_data: + return + + # Clear previous content + self.summary_text.delete(1.0, tk.END) + self.reconcile_text.delete(1.0, tk.END) + + data = self.comparison_data + + # Summary text + summary = f"""COMPARISON SUMMARY +{'='*50} + +Original Counts: + KST Total: {data['original_counts']['kst_total']:,} + Coordi Total: {data['original_counts']['coordi_total']:,} + +Matched Items: {data['matched_items_count']:,} + +Mismatches: + KST Only: {data['mismatches']['kst_only_count']:,} + Coordi Only: {data['mismatches']['coordi_only_count']:,} + KST Duplicates: {data['mismatches']['kst_duplicates_count']:,} + Coordi Duplicates: {data['mismatches']['coordi_duplicates_count']:,} + +Total Mismatches: {data['mismatches']['kst_only_count'] + data['mismatches']['coordi_only_count'] + data['mismatches']['kst_duplicates_count'] + data['mismatches']['coordi_duplicates_count']:,} +""" + + self.summary_text.insert(tk.END, summary) + + # Reconciliation text + reconcile = data['reconciliation'] + reconcile_info = f"""RECONCILIATION RESULTS +{'='*40} + +After excluding mismatches: + KST Count: {reconcile['reconciled_kst_count']:,} + Coordi Count: {reconcile['reconciled_coordi_count']:,} + Counts Match: {'✅ YES' if reconcile['counts_match_after_reconciliation'] else '❌ NO'} + +Items to exclude: + From KST: {reconcile['items_to_exclude_from_kst']:,} + From Coordi: {reconcile['items_to_exclude_from_coordi']:,} + +Final Result: Both datasets will have {reconcile['reconciled_kst_count']:,} matching items after reconciliation. +""" + + self.reconcile_text.insert(tk.END, reconcile_info) + + def update_data_tables(self): + if not self.comparison_data: + return + + mismatches = self.comparison_data['mismatch_details'] + + # Update matched items (create from intersection) + matched_count = self.comparison_data['matched_items_count'] + self.matched_count_var.set(f"{matched_count:,}") + + # Clear matched tree + for item in self.matched_tree.get_children(): + self.matched_tree.delete(item) + + # Add matched items (we'll show the first few as examples) + if self.comparator: + categorization = self.comparator.categorize_mismatches() + matched_items = categorization['matched_items'] + for i, item in enumerate(list(matched_items)[:100]): # Show first 100 + self.matched_tree.insert("", tk.END, values=( + item.title, item.episode, item.source_sheet, item.row_index + 1, "Perfect match" + )) + + # Update KST only + kst_only = mismatches['kst_only'] + self.kst_only_count_var.set(f"{len(kst_only):,}") + + for item in self.kst_only_tree.get_children(): + self.kst_only_tree.delete(item) + + for mismatch in kst_only: + self.kst_only_tree.insert("", tk.END, values=( + mismatch['title'], mismatch['episode'], mismatch['sheet'], + mismatch['row_index'] + 1, mismatch['reason'] + )) + + # Update Coordi only + coordi_only = mismatches['coordi_only'] + self.coordi_only_count_var.set(f"{len(coordi_only):,}") + + for item in self.coordi_only_tree.get_children(): + self.coordi_only_tree.delete(item) + + for mismatch in coordi_only: + self.coordi_only_tree.insert("", tk.END, values=( + mismatch['title'], mismatch['episode'], mismatch['sheet'], + mismatch['row_index'] + 1, mismatch['reason'] + )) + +def main(): + root = tk.Tk() + app = DataComparisonGUI(root) + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..9947ae7 --- /dev/null +++ b/main.py @@ -0,0 +1,8 @@ +from web_gui import main as web_gui_main + +def main(): + print("Launching KST vs Coordi Data Comparison Web GUI...") + web_gui_main() + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..14d071b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "data-comparison" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "pandas>=2.0.0", + "openpyxl>=3.1.0", + "flask>=2.3.0", + "requests>=2.25.0" +] diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..f916ec0 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,515 @@ + + + + + + KST vs Coordi Data Comparison + + + +
+

KST vs Coordi Data Comparison Tool

+ +
+
+ + + +
+
+ + + +
+
+ + +
+
+
+ + +
+ + + + \ No newline at end of file diff --git a/test_grouping.py b/test_grouping.py new file mode 100644 index 0000000..1716daf --- /dev/null +++ b/test_grouping.py @@ -0,0 +1,28 @@ +from data_comparator import KSTCoordiComparator + +def test_grouping(): + comparator = KSTCoordiComparator('data/sample-data.xlsx') + if comparator.load_data(): + summary = comparator.get_comparison_summary() + + print("=== GROUPED BY TITLE TEST ===") + print(f"Total unique titles: {len(summary['grouped_by_title']['title_summaries'])}") + print() + + # Show top 5 titles with worst match percentages + sorted_titles = sorted( + summary['grouped_by_title']['title_summaries'].items(), + key=lambda x: x[1]['match_percentage'] + ) + + print("Top 5 titles needing attention (worst match %):") + for i, (title, data) in enumerate(sorted_titles[:5]): + print(f"{i+1}. {title}") + print(f" Total Episodes: {data['total_episodes']}") + print(f" Matched: {data['matched_count']} ({data['match_percentage']}%)") + print(f" KST Only: {data['kst_only_count']}") + print(f" Coordi Only: {data['coordi_only_count']}") + print() + +if __name__ == "__main__": + test_grouping() \ No newline at end of file diff --git a/test_sheet_filtering.py b/test_sheet_filtering.py new file mode 100644 index 0000000..63109a6 --- /dev/null +++ b/test_sheet_filtering.py @@ -0,0 +1,38 @@ +from data_comparator import KSTCoordiComparator + +def test_sheet_filtering(): + comparator = KSTCoordiComparator('data/sample-data.xlsx') + if comparator.load_data(): + print("=== SHEET FILTERING TEST ===") + + # Test All Sheets + summary_all = comparator.get_comparison_summary() + print(f"All Sheets:") + print(f" Available sheets: {summary_all['sheet_names']}") + print(f" Matched items: {summary_all['matched_items_count']}") + print(f" KST only: {summary_all['mismatches']['kst_only_count']}") + print(f" Coordi only: {summary_all['mismatches']['coordi_only_count']}") + print() + + # Test TH URGENT only + summary_th = comparator.get_comparison_summary('TH URGENT') + print(f"TH URGENT only:") + print(f" Matched items: {summary_th['matched_items_count']}") + print(f" KST only: {summary_th['mismatches']['kst_only_count']}") + print(f" Coordi only: {summary_th['mismatches']['coordi_only_count']}") + print() + + # Test US URGENT only + summary_us = comparator.get_comparison_summary('US URGENT') + print(f"US URGENT only:") + print(f" Matched items: {summary_us['matched_items_count']}") + print(f" KST only: {summary_us['mismatches']['kst_only_count']}") + print(f" Coordi only: {summary_us['mismatches']['coordi_only_count']}") + print() + + print("✓ Sheet filtering functionality working!") + print("✓ Web GUI ready at http://localhost:8080") + print("✓ Dropdown will show: All Sheets, TH URGENT, US URGENT") + +if __name__ == "__main__": + test_sheet_filtering() \ No newline at end of file diff --git a/test_simplified_gui.py b/test_simplified_gui.py new file mode 100644 index 0000000..8188dde --- /dev/null +++ b/test_simplified_gui.py @@ -0,0 +1,35 @@ +from data_comparator import KSTCoordiComparator + +def test_simplified_interface(): + comparator = KSTCoordiComparator('data/sample-data.xlsx') + if comparator.load_data(): + summary = comparator.get_comparison_summary() + + print("=== SIMPLIFIED INTERFACE TEST ===") + print(f"Matched items (Summary tab): {summary['matched_items_count']}") + + total_different = (summary['mismatches']['kst_only_count'] + + summary['mismatches']['coordi_only_count'] + + summary['mismatches']['kst_duplicates_count'] + + summary['mismatches']['coordi_duplicates_count']) + print(f"Different items (Different tab): {total_different}") + print() + + print("Sample items from Different tab (first 5):") + + # Show KST-only items + kst_only = summary['mismatch_details']['kst_only'][:3] + for item in kst_only: + print(f"KST: {item['title']} - Episode {item['episode']} | Coordi: (empty) | Reason: Only appears in KST") + + # Show Coordi-only items + coordi_only = summary['mismatch_details']['coordi_only'][:2] + for item in coordi_only: + print(f"KST: (empty) | Coordi: {item['title']} - Episode {item['episode']} | Reason: Only appears in Coordi") + + print(f"\n✅ Interface ready at http://localhost:8080") + print("✅ Two tabs: Summary (matched items) and Different (mismatches)") + print("✅ Korean title + episode sorting implemented") + +if __name__ == "__main__": + test_simplified_interface() \ No newline at end of file diff --git a/test_upload.py b/test_upload.py new file mode 100644 index 0000000..4e422fa --- /dev/null +++ b/test_upload.py @@ -0,0 +1,58 @@ +import requests +import os + +def test_file_upload(): + # Test the upload endpoint + file_path = 'data/sample-data.xlsx' + + if not os.path.exists(file_path): + print("Error: Sample file not found") + return + + print("=== FILE UPLOAD TEST ===") + print(f"Testing upload of: {file_path}") + + # Test upload endpoint + with open(file_path, 'rb') as f: + files = {'file': f} + try: + response = requests.post('http://localhost:8080/upload', files=files, timeout=30) + + if response.status_code == 200: + data = response.json() + if data.get('success'): + print("✓ File upload successful!") + print(f" Uploaded filename: {data['filename']}") + print(f" Server file path: {data['file_path']}") + + # Test analysis of uploaded file + print("✓ Testing analysis of uploaded file...") + analyze_response = requests.post('http://localhost:8080/analyze', + json={'file_path': data['file_path']}, + timeout=30) + + if analyze_response.status_code == 200: + analyze_data = analyze_response.json() + if analyze_data.get('success'): + print("✓ Analysis of uploaded file successful!") + results = analyze_data['results'] + print(f" Sheets found: {results['sheet_names']}") + print(f" Matched items: {results['matched_items_count']}") + print(f" Different items: {results['mismatches']['kst_only_count'] + results['mismatches']['coordi_only_count']}") + else: + print(f"✗ Analysis failed: {analyze_data.get('error')}") + else: + print(f"✗ Analysis request failed: {analyze_response.status_code}") + + else: + print(f"✗ Upload failed: {data.get('error')}") + else: + print(f"✗ Upload request failed: {response.status_code}") + print(f"Response: {response.text}") + + except requests.exceptions.RequestException as e: + print(f"✗ Request failed: {e}") + print("Make sure the Flask server is running at http://localhost:8080") + +if __name__ == "__main__": + test_file_upload() \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..8e01476 --- /dev/null +++ b/uv.lock @@ -0,0 +1,342 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "data-comparison" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "flask" }, + { name = "openpyxl" }, + { name = "pandas" }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "flask", specifier = ">=2.3.0" }, + { name = "openpyxl", specifier = ">=3.1.0" }, + { name = "pandas", specifier = ">=2.0.0" }, + { name = "requests", specifier = ">=2.25.0" }, +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" }, + { url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" }, + { url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" }, + { url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" }, + { url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" }, + { url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, + { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, + { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, + { url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, + { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" }, + { url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" }, + { url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" }, + { url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" }, + { url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, +] diff --git a/web_gui.py b/web_gui.py new file mode 100644 index 0000000..eabea27 --- /dev/null +++ b/web_gui.py @@ -0,0 +1,637 @@ +from flask import Flask, render_template, request, jsonify +import json +import os +import tempfile +from pathlib import Path +from werkzeug.utils import secure_filename +from data_comparator import KSTCoordiComparator + +app = Flask(__name__) +app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB max file size +app.config['UPLOAD_FOLDER'] = tempfile.gettempdir() + +# Global variable to store comparison results +comparison_results = None +comparator_instance = None + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/analyze', methods=['POST']) +def analyze_data(): + global comparison_results, comparator_instance + + try: + file_path = request.json.get('file_path', 'data/sample-data.xlsx') + sheet_filter = request.json.get('sheet_filter', None) + + if not Path(file_path).exists(): + return jsonify({'error': f'File not found: {file_path}'}), 400 + + # Create comparator and analyze + comparator_instance = KSTCoordiComparator(file_path) + if not comparator_instance.load_data(): + return jsonify({'error': 'Failed to load Excel data'}), 500 + + # Get comparison results with optional sheet filtering + comparison_results = comparator_instance.get_comparison_summary(sheet_filter) + + # Get matched items for display + categorization = comparator_instance.categorize_mismatches() + matched_items = list(categorization['matched_items']) + + # Filter matched items by sheet if specified + if sheet_filter and sheet_filter != 'All Sheets': + matched_items = [item for item in matched_items if item.source_sheet == sheet_filter] + + # Format matched items for JSON (limit to first 500 for performance) + matched_data = [] + for item in matched_items[:500]: + matched_data.append({ + 'title': item.title, + 'episode': item.episode, + 'sheet': item.source_sheet, + 'row': item.row_index + 1, + 'reason': 'Perfect match' + }) + + # Add matched data to results + comparison_results['matched_data'] = matched_data + comparison_results['matched_items_count'] = len(matched_items) # Update count for filtered data + + return jsonify({ + 'success': True, + 'results': comparison_results + }) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/upload', methods=['POST']) +def upload_file(): + try: + if 'file' not in request.files: + return jsonify({'error': 'No file provided'}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({'error': 'No file selected'}), 400 + + if file and file.filename.lower().endswith(('.xlsx', '.xls')): + # Save uploaded file + filename = secure_filename(file.filename) + file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(file_path) + + return jsonify({ + 'success': True, + 'file_path': file_path, + 'filename': filename + }) + else: + return jsonify({'error': 'Please upload an Excel file (.xlsx or .xls)'}), 400 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/get_results') +def get_results(): + if comparison_results is None: + return jsonify({'error': 'No analysis results available'}), 404 + return jsonify(comparison_results) + +def create_templates_dir(): + """Create templates directory and HTML file""" + templates_dir = Path('templates') + templates_dir.mkdir(exist_ok=True) + + html_content = ''' + + + + + KST vs Coordi Data Comparison + + + +
+

KST vs Coordi Data Comparison Tool

+ +
+
+ + + +
+
+ + + +
+
+ + +
+
+
+ + +
+ + + +''' + + html_file = templates_dir / 'index.html' + html_file.write_text(html_content) + +def main(): + # Create templates directory and HTML file + create_templates_dir() + + print("Starting web-based GUI...") + print("Open your browser and go to: http://localhost:8080") + app.run(debug=True, host='0.0.0.0', port=8080) + +if __name__ == "__main__": + main() \ No newline at end of file