first push
This commit is contained in:
commit
7df12119fb
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.13
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"kiroAgent.configureMCP": "Disabled"
|
||||
}
|
||||
59
CLAUDE.md
Normal file
59
CLAUDE.md
Normal file
@ -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
|
||||
92
analyze_excel.py
Normal file
92
analyze_excel.py
Normal file
@ -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)
|
||||
BIN
data/sample-data.xlsx
Normal file
BIN
data/sample-data.xlsx
Normal file
Binary file not shown.
BIN
data/~$sample-data.xlsx
Normal file
BIN
data/~$sample-data.xlsx
Normal file
Binary file not shown.
488
data_comparator.py
Normal file
488
data_comparator.py
Normal file
@ -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!")
|
||||
319
gui_app.py
Normal file
319
gui_app.py
Normal file
@ -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()
|
||||
8
main.py
Normal file
8
main.py
Normal file
@ -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()
|
||||
12
pyproject.toml
Normal file
12
pyproject.toml
Normal file
@ -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"
|
||||
]
|
||||
515
templates/index.html
Normal file
515
templates/index.html
Normal file
@ -0,0 +1,515 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KST vs Coordi Data Comparison</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.file-section {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.file-input {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
input[type="text"], input[type="file"] {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
input[type="file"] {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
.tabs {
|
||||
border-bottom: 2px solid #ddd;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.tab {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom: none;
|
||||
cursor: pointer;
|
||||
margin-right: 5px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
.tab.active {
|
||||
background: white;
|
||||
border-bottom: 2px solid white;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.summary-card {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
.summary-card h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
.count-badge {
|
||||
display: inline-block;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.9em;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.reconciliation {
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.reconciliation.mismatch {
|
||||
background: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
th, td {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.table-container {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>KST vs Coordi Data Comparison Tool</h1>
|
||||
|
||||
<div class="file-section">
|
||||
<div class="file-input">
|
||||
<label for="filePath">Excel File Path:</label>
|
||||
<input type="text" id="filePath" value="data/sample-data.xlsx" placeholder="Enter file path">
|
||||
<button onclick="analyzeData()" id="analyzeBtn">Analyze Data</button>
|
||||
</div>
|
||||
<div class="file-input" style="margin-top: 10px;">
|
||||
<label>Or Upload File:</label>
|
||||
<input type="file" id="fileUpload" accept=".xlsx,.xls" onchange="handleFileUpload()">
|
||||
<button onclick="uploadAndAnalyze()" id="uploadBtn" disabled>Upload & Analyze</button>
|
||||
</div>
|
||||
<div class="file-input" style="margin-top: 10px;">
|
||||
<label for="sheetFilter">Sheet Filter:</label>
|
||||
<select id="sheetFilter" onchange="filterBySheet()" disabled>
|
||||
<option value="All Sheets">All Sheets</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
|
||||
<div id="results" style="display: none;">
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="showTab('summary')">Summary</div>
|
||||
<div class="tab" onclick="showTab('different')">Different</div>
|
||||
</div>
|
||||
|
||||
<div id="summary" class="tab-content active">
|
||||
<h3>Matched Items (Same in both KST and Coordi) <span id="matched-count-display" class="count-badge">0</span></h3>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Korean Title</th>
|
||||
<th>Episode</th>
|
||||
<th>Sheet</th>
|
||||
<th>Row</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="summary-table">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="different" class="tab-content">
|
||||
<h3>Different Items <span id="different-count-display" class="count-badge">0</span></h3>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>KST Data</th>
|
||||
<th>Coordi Data</th>
|
||||
<th>Reason</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="different-table">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showTab(tabName) {
|
||||
// Hide all tab contents
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// Remove active class from all tabs
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show selected tab content
|
||||
document.getElementById(tabName).classList.add('active');
|
||||
|
||||
// Add active class to clicked tab
|
||||
event.target.classList.add('active');
|
||||
}
|
||||
|
||||
function analyzeData() {
|
||||
const filePath = document.getElementById('filePath').value;
|
||||
const sheetFilter = document.getElementById('sheetFilter').value;
|
||||
const statusDiv = document.getElementById('status');
|
||||
const analyzeBtn = document.getElementById('analyzeBtn');
|
||||
|
||||
if (!filePath.trim()) {
|
||||
statusDiv.innerHTML = '<div class="error">Please enter a file path</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
statusDiv.innerHTML = '<div class="loading">Analyzing data...</div>';
|
||||
analyzeBtn.disabled = true;
|
||||
analyzeBtn.textContent = 'Analyzing...';
|
||||
|
||||
// Make API call
|
||||
fetch('/analyze', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_path: filePath,
|
||||
sheet_filter: sheetFilter === 'All Sheets' ? null : sheetFilter
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
statusDiv.innerHTML = '<div class="success">Analysis complete!</div>';
|
||||
updateResults(data.results);
|
||||
updateSheetFilter(data.results.sheet_names, data.results.current_sheet_filter);
|
||||
document.getElementById('results').style.display = 'block';
|
||||
} else {
|
||||
statusDiv.innerHTML = `<div class="error">Error: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
statusDiv.innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
||||
})
|
||||
.finally(() => {
|
||||
analyzeBtn.disabled = false;
|
||||
analyzeBtn.textContent = 'Analyze Data';
|
||||
});
|
||||
}
|
||||
|
||||
function updateSheetFilter(sheetNames, currentFilter) {
|
||||
const select = document.getElementById('sheetFilter');
|
||||
select.innerHTML = '<option value="All Sheets">All Sheets</option>';
|
||||
|
||||
sheetNames.forEach(sheetName => {
|
||||
const option = document.createElement('option');
|
||||
option.value = sheetName;
|
||||
option.textContent = sheetName;
|
||||
if (sheetName === currentFilter) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
select.disabled = false;
|
||||
}
|
||||
|
||||
function filterBySheet() {
|
||||
// Re-analyze with the selected sheet filter
|
||||
analyzeData();
|
||||
}
|
||||
|
||||
function handleFileUpload() {
|
||||
const fileInput = document.getElementById('fileUpload');
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
|
||||
if (fileInput.files.length > 0) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = 'Upload & Analyze';
|
||||
} else {
|
||||
uploadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function uploadAndAnalyze() {
|
||||
const fileInput = document.getElementById('fileUpload');
|
||||
const statusDiv = document.getElementById('status');
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
|
||||
if (fileInput.files.length === 0) {
|
||||
statusDiv.innerHTML = '<div class="error">Please select a file to upload</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fileInput.files[0];
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// Show uploading state
|
||||
statusDiv.innerHTML = '<div class="loading">Uploading and analyzing file...</div>';
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.textContent = 'Uploading...';
|
||||
|
||||
// Upload file
|
||||
fetch('/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// File uploaded successfully, now analyze it
|
||||
document.getElementById('filePath').value = data.file_path;
|
||||
statusDiv.innerHTML = '<div class="loading">File uploaded! Analyzing data...</div>';
|
||||
|
||||
// Analyze the uploaded file
|
||||
const sheetFilter = document.getElementById('sheetFilter').value;
|
||||
return fetch('/analyze', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_path: data.file_path,
|
||||
sheet_filter: sheetFilter === 'All Sheets' ? null : sheetFilter
|
||||
})
|
||||
});
|
||||
} else {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
statusDiv.innerHTML = '<div class="success">File uploaded and analyzed successfully!</div>';
|
||||
updateResults(data.results);
|
||||
updateSheetFilter(data.results.sheet_names, data.results.current_sheet_filter);
|
||||
document.getElementById('results').style.display = 'block';
|
||||
} else {
|
||||
statusDiv.innerHTML = `<div class="error">Analysis error: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
statusDiv.innerHTML = `<div class="error">Upload error: ${error.message}</div>`;
|
||||
})
|
||||
.finally(() => {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = 'Upload & Analyze';
|
||||
handleFileUpload(); // Reset button state based on file selection
|
||||
});
|
||||
}
|
||||
|
||||
function updateResults(results) {
|
||||
// Update count displays
|
||||
document.getElementById('matched-count-display').textContent = results.matched_items_count.toLocaleString();
|
||||
|
||||
const totalDifferent = results.mismatches.kst_only_count + results.mismatches.coordi_only_count +
|
||||
results.mismatches.kst_duplicates_count + results.mismatches.coordi_duplicates_count;
|
||||
document.getElementById('different-count-display').textContent = totalDifferent.toLocaleString();
|
||||
|
||||
// Update Summary tab (matched items)
|
||||
updateSummaryTable(results.matched_data);
|
||||
|
||||
// Update Different tab
|
||||
updateDifferentTable(results.mismatch_details);
|
||||
}
|
||||
|
||||
function updateSummaryTable(matchedData) {
|
||||
const tbody = document.getElementById('summary-table');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Sort by Korean title + episode
|
||||
const sortedData = [...matchedData].sort((a, b) => {
|
||||
const titleCompare = a.title.localeCompare(b.title, 'ko');
|
||||
if (titleCompare !== 0) return titleCompare;
|
||||
|
||||
// Try to sort episodes numerically
|
||||
const aEp = parseFloat(a.episode) || 0;
|
||||
const bEp = parseFloat(b.episode) || 0;
|
||||
return aEp - bEp;
|
||||
});
|
||||
|
||||
sortedData.forEach(item => {
|
||||
const row = tbody.insertRow();
|
||||
row.insertCell(0).textContent = item.title;
|
||||
row.insertCell(1).textContent = item.episode;
|
||||
row.insertCell(2).textContent = item.sheet;
|
||||
row.insertCell(3).textContent = item.row_index ? item.row_index + 1 : item.row;
|
||||
});
|
||||
}
|
||||
|
||||
function updateDifferentTable(mismatchDetails) {
|
||||
const tbody = document.getElementById('different-table');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Combine all mismatches into one array for sorting
|
||||
const allDifferences = [];
|
||||
|
||||
// Add KST-only items
|
||||
mismatchDetails.kst_only.forEach(item => {
|
||||
allDifferences.push({
|
||||
kstData: `${item.title} - Episode ${item.episode}`,
|
||||
coordiData: '',
|
||||
reason: 'Only appears in KST',
|
||||
sortTitle: item.title,
|
||||
sortEpisode: parseFloat(item.episode) || 0
|
||||
});
|
||||
});
|
||||
|
||||
// Add Coordi-only items
|
||||
mismatchDetails.coordi_only.forEach(item => {
|
||||
allDifferences.push({
|
||||
kstData: '',
|
||||
coordiData: `${item.title} - Episode ${item.episode}`,
|
||||
reason: 'Only appears in Coordi',
|
||||
sortTitle: item.title,
|
||||
sortEpisode: parseFloat(item.episode) || 0
|
||||
});
|
||||
});
|
||||
|
||||
// Add KST duplicates
|
||||
mismatchDetails.kst_duplicates.forEach(item => {
|
||||
allDifferences.push({
|
||||
kstData: `${item.title} - Episode ${item.episode}`,
|
||||
coordiData: '',
|
||||
reason: 'Duplicate in KST',
|
||||
sortTitle: item.title,
|
||||
sortEpisode: parseFloat(item.episode) || 0
|
||||
});
|
||||
});
|
||||
|
||||
// Add Coordi duplicates
|
||||
mismatchDetails.coordi_duplicates.forEach(item => {
|
||||
allDifferences.push({
|
||||
kstData: '',
|
||||
coordiData: `${item.title} - Episode ${item.episode}`,
|
||||
reason: 'Duplicate in Coordi',
|
||||
sortTitle: item.title,
|
||||
sortEpisode: parseFloat(item.episode) || 0
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by Korean title + episode
|
||||
allDifferences.sort((a, b) => {
|
||||
const titleCompare = a.sortTitle.localeCompare(b.sortTitle, 'ko');
|
||||
if (titleCompare !== 0) return titleCompare;
|
||||
return a.sortEpisode - b.sortEpisode;
|
||||
});
|
||||
|
||||
// Populate table
|
||||
allDifferences.forEach(diff => {
|
||||
const row = tbody.insertRow();
|
||||
row.insertCell(0).textContent = diff.kstData;
|
||||
row.insertCell(1).textContent = diff.coordiData;
|
||||
row.insertCell(2).textContent = diff.reason;
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-analyze on page load with default file
|
||||
window.onload = function() {
|
||||
analyzeData();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
28
test_grouping.py
Normal file
28
test_grouping.py
Normal file
@ -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()
|
||||
38
test_sheet_filtering.py
Normal file
38
test_sheet_filtering.py
Normal file
@ -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()
|
||||
35
test_simplified_gui.py
Normal file
35
test_simplified_gui.py
Normal file
@ -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()
|
||||
58
test_upload.py
Normal file
58
test_upload.py
Normal file
@ -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()
|
||||
342
uv.lock
generated
Normal file
342
uv.lock
generated
Normal file
@ -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" },
|
||||
]
|
||||
637
web_gui.py
Normal file
637
web_gui.py
Normal file
@ -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 = '''<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KST vs Coordi Data Comparison</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.file-section {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.file-input {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
input[type="text"], input[type="file"] {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
input[type="file"] {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
.tabs {
|
||||
border-bottom: 2px solid #ddd;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.tab {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom: none;
|
||||
cursor: pointer;
|
||||
margin-right: 5px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
.tab.active {
|
||||
background: white;
|
||||
border-bottom: 2px solid white;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.summary-card {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
.summary-card h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
.count-badge {
|
||||
display: inline-block;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.9em;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.reconciliation {
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.reconciliation.mismatch {
|
||||
background: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
th, td {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.table-container {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>KST vs Coordi Data Comparison Tool</h1>
|
||||
|
||||
<div class="file-section">
|
||||
<div class="file-input">
|
||||
<label for="filePath">Excel File Path:</label>
|
||||
<input type="text" id="filePath" value="data/sample-data.xlsx" placeholder="Enter file path">
|
||||
<button onclick="analyzeData()" id="analyzeBtn">Analyze Data</button>
|
||||
</div>
|
||||
<div class="file-input" style="margin-top: 10px;">
|
||||
<label>Or Upload File:</label>
|
||||
<input type="file" id="fileUpload" accept=".xlsx,.xls" onchange="handleFileUpload()">
|
||||
<button onclick="uploadAndAnalyze()" id="uploadBtn" disabled>Upload & Analyze</button>
|
||||
</div>
|
||||
<div class="file-input" style="margin-top: 10px;">
|
||||
<label for="sheetFilter">Sheet Filter:</label>
|
||||
<select id="sheetFilter" onchange="filterBySheet()" disabled>
|
||||
<option value="All Sheets">All Sheets</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
|
||||
<div id="results" style="display: none;">
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="showTab('summary')">Summary</div>
|
||||
<div class="tab" onclick="showTab('different')">Different</div>
|
||||
</div>
|
||||
|
||||
<div id="summary" class="tab-content active">
|
||||
<h3>Matched Items (Same in both KST and Coordi) <span id="matched-count-display" class="count-badge">0</span></h3>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Korean Title</th>
|
||||
<th>Episode</th>
|
||||
<th>Sheet</th>
|
||||
<th>Row</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="summary-table">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="different" class="tab-content">
|
||||
<h3>Different Items <span id="different-count-display" class="count-badge">0</span></h3>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>KST Data</th>
|
||||
<th>Coordi Data</th>
|
||||
<th>Reason</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="different-table">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showTab(tabName) {
|
||||
// Hide all tab contents
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// Remove active class from all tabs
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show selected tab content
|
||||
document.getElementById(tabName).classList.add('active');
|
||||
|
||||
// Add active class to clicked tab
|
||||
event.target.classList.add('active');
|
||||
}
|
||||
|
||||
function analyzeData() {
|
||||
const filePath = document.getElementById('filePath').value;
|
||||
const sheetFilter = document.getElementById('sheetFilter').value;
|
||||
const statusDiv = document.getElementById('status');
|
||||
const analyzeBtn = document.getElementById('analyzeBtn');
|
||||
|
||||
if (!filePath.trim()) {
|
||||
statusDiv.innerHTML = '<div class="error">Please enter a file path</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
statusDiv.innerHTML = '<div class="loading">Analyzing data...</div>';
|
||||
analyzeBtn.disabled = true;
|
||||
analyzeBtn.textContent = 'Analyzing...';
|
||||
|
||||
// Make API call
|
||||
fetch('/analyze', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_path: filePath,
|
||||
sheet_filter: sheetFilter === 'All Sheets' ? null : sheetFilter
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
statusDiv.innerHTML = '<div class="success">Analysis complete!</div>';
|
||||
updateResults(data.results);
|
||||
updateSheetFilter(data.results.sheet_names, data.results.current_sheet_filter);
|
||||
document.getElementById('results').style.display = 'block';
|
||||
} else {
|
||||
statusDiv.innerHTML = `<div class="error">Error: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
statusDiv.innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
||||
})
|
||||
.finally(() => {
|
||||
analyzeBtn.disabled = false;
|
||||
analyzeBtn.textContent = 'Analyze Data';
|
||||
});
|
||||
}
|
||||
|
||||
function updateSheetFilter(sheetNames, currentFilter) {
|
||||
const select = document.getElementById('sheetFilter');
|
||||
select.innerHTML = '<option value="All Sheets">All Sheets</option>';
|
||||
|
||||
sheetNames.forEach(sheetName => {
|
||||
const option = document.createElement('option');
|
||||
option.value = sheetName;
|
||||
option.textContent = sheetName;
|
||||
if (sheetName === currentFilter) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
select.disabled = false;
|
||||
}
|
||||
|
||||
function filterBySheet() {
|
||||
// Re-analyze with the selected sheet filter
|
||||
analyzeData();
|
||||
}
|
||||
|
||||
function handleFileUpload() {
|
||||
const fileInput = document.getElementById('fileUpload');
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
|
||||
if (fileInput.files.length > 0) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = 'Upload & Analyze';
|
||||
} else {
|
||||
uploadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function uploadAndAnalyze() {
|
||||
const fileInput = document.getElementById('fileUpload');
|
||||
const statusDiv = document.getElementById('status');
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
|
||||
if (fileInput.files.length === 0) {
|
||||
statusDiv.innerHTML = '<div class="error">Please select a file to upload</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fileInput.files[0];
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// Show uploading state
|
||||
statusDiv.innerHTML = '<div class="loading">Uploading and analyzing file...</div>';
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.textContent = 'Uploading...';
|
||||
|
||||
// Upload file
|
||||
fetch('/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// File uploaded successfully, now analyze it
|
||||
document.getElementById('filePath').value = data.file_path;
|
||||
statusDiv.innerHTML = '<div class="loading">File uploaded! Analyzing data...</div>';
|
||||
|
||||
// Analyze the uploaded file
|
||||
const sheetFilter = document.getElementById('sheetFilter').value;
|
||||
return fetch('/analyze', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_path: data.file_path,
|
||||
sheet_filter: sheetFilter === 'All Sheets' ? null : sheetFilter
|
||||
})
|
||||
});
|
||||
} else {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
statusDiv.innerHTML = '<div class="success">File uploaded and analyzed successfully!</div>';
|
||||
updateResults(data.results);
|
||||
updateSheetFilter(data.results.sheet_names, data.results.current_sheet_filter);
|
||||
document.getElementById('results').style.display = 'block';
|
||||
} else {
|
||||
statusDiv.innerHTML = `<div class="error">Analysis error: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
statusDiv.innerHTML = `<div class="error">Upload error: ${error.message}</div>`;
|
||||
})
|
||||
.finally(() => {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = 'Upload & Analyze';
|
||||
handleFileUpload(); // Reset button state based on file selection
|
||||
});
|
||||
}
|
||||
|
||||
function updateResults(results) {
|
||||
// Update count displays
|
||||
document.getElementById('matched-count-display').textContent = results.matched_items_count.toLocaleString();
|
||||
|
||||
const totalDifferent = results.mismatches.kst_only_count + results.mismatches.coordi_only_count +
|
||||
results.mismatches.kst_duplicates_count + results.mismatches.coordi_duplicates_count;
|
||||
document.getElementById('different-count-display').textContent = totalDifferent.toLocaleString();
|
||||
|
||||
// Update Summary tab (matched items)
|
||||
updateSummaryTable(results.matched_data);
|
||||
|
||||
// Update Different tab
|
||||
updateDifferentTable(results.mismatch_details);
|
||||
}
|
||||
|
||||
function updateSummaryTable(matchedData) {
|
||||
const tbody = document.getElementById('summary-table');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Sort by Korean title + episode
|
||||
const sortedData = [...matchedData].sort((a, b) => {
|
||||
const titleCompare = a.title.localeCompare(b.title, 'ko');
|
||||
if (titleCompare !== 0) return titleCompare;
|
||||
|
||||
// Try to sort episodes numerically
|
||||
const aEp = parseFloat(a.episode) || 0;
|
||||
const bEp = parseFloat(b.episode) || 0;
|
||||
return aEp - bEp;
|
||||
});
|
||||
|
||||
sortedData.forEach(item => {
|
||||
const row = tbody.insertRow();
|
||||
row.insertCell(0).textContent = item.title;
|
||||
row.insertCell(1).textContent = item.episode;
|
||||
row.insertCell(2).textContent = item.sheet;
|
||||
row.insertCell(3).textContent = item.row_index ? item.row_index + 1 : item.row;
|
||||
});
|
||||
}
|
||||
|
||||
function updateDifferentTable(mismatchDetails) {
|
||||
const tbody = document.getElementById('different-table');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Combine all mismatches into one array for sorting
|
||||
const allDifferences = [];
|
||||
|
||||
// Add KST-only items
|
||||
mismatchDetails.kst_only.forEach(item => {
|
||||
allDifferences.push({
|
||||
kstData: `${item.title} - Episode ${item.episode}`,
|
||||
coordiData: '',
|
||||
reason: 'Only appears in KST',
|
||||
sortTitle: item.title,
|
||||
sortEpisode: parseFloat(item.episode) || 0
|
||||
});
|
||||
});
|
||||
|
||||
// Add Coordi-only items
|
||||
mismatchDetails.coordi_only.forEach(item => {
|
||||
allDifferences.push({
|
||||
kstData: '',
|
||||
coordiData: `${item.title} - Episode ${item.episode}`,
|
||||
reason: 'Only appears in Coordi',
|
||||
sortTitle: item.title,
|
||||
sortEpisode: parseFloat(item.episode) || 0
|
||||
});
|
||||
});
|
||||
|
||||
// Add KST duplicates
|
||||
mismatchDetails.kst_duplicates.forEach(item => {
|
||||
allDifferences.push({
|
||||
kstData: `${item.title} - Episode ${item.episode}`,
|
||||
coordiData: '',
|
||||
reason: 'Duplicate in KST',
|
||||
sortTitle: item.title,
|
||||
sortEpisode: parseFloat(item.episode) || 0
|
||||
});
|
||||
});
|
||||
|
||||
// Add Coordi duplicates
|
||||
mismatchDetails.coordi_duplicates.forEach(item => {
|
||||
allDifferences.push({
|
||||
kstData: '',
|
||||
coordiData: `${item.title} - Episode ${item.episode}`,
|
||||
reason: 'Duplicate in Coordi',
|
||||
sortTitle: item.title,
|
||||
sortEpisode: parseFloat(item.episode) || 0
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by Korean title + episode
|
||||
allDifferences.sort((a, b) => {
|
||||
const titleCompare = a.sortTitle.localeCompare(b.sortTitle, 'ko');
|
||||
if (titleCompare !== 0) return titleCompare;
|
||||
return a.sortEpisode - b.sortEpisode;
|
||||
});
|
||||
|
||||
// Populate table
|
||||
allDifferences.forEach(diff => {
|
||||
const row = tbody.insertRow();
|
||||
row.insertCell(0).textContent = diff.kstData;
|
||||
row.insertCell(1).textContent = diff.coordiData;
|
||||
row.insertCell(2).textContent = diff.reason;
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-analyze on page load with default file
|
||||
window.onload = function() {
|
||||
analyzeData();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
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()
|
||||
Loading…
Reference in New Issue
Block a user