From b7a22524d70c3f2a0e977fa351e3980dded118f8 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sat, 2 Aug 2025 10:00:24 -0700 Subject: [PATCH] Feat: add export functionality with GUI for data and graphs - Implemented ExportWindow class for exporting data and graphs in various formats (JSON, XML, PDF). - Integrated ExportManager to handle export logic. - Added export option in the main application menu. - Enhanced user interface with data summary and export options. - Included error handling and success messages for export operations. - Updated dependencies in the lock file to include reportlab and lxml for PDF generation. --- .gitignore | 1 + Makefile | 2 +- README.md | 8 + docs/EXPORT_SYSTEM.md | 215 ++++++++++++++++++++ pyproject.toml | 4 +- scripts/README.md | 61 ++++++ scripts/integration_test.py | 128 ++++++++++++ src/export_manager.py | 385 ++++++++++++++++++++++++++++++++++++ src/export_window.py | 247 +++++++++++++++++++++++ src/main.py | 22 +++ uv.lock | 65 +++++- 11 files changed, 1135 insertions(+), 3 deletions(-) create mode 100644 docs/EXPORT_SYSTEM.md create mode 100644 scripts/README.md create mode 100644 scripts/integration_test.py create mode 100644 src/export_manager.py create mode 100644 src/export_window.py diff --git a/.gitignore b/.gitignore index 133b9af..362b37e 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,4 @@ Thumbs.db .Trashes ehthumbs.db Thumbs.db +integration_test_exports/ diff --git a/Makefile b/Makefile index c864402..ba27181 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ TARGET=thechart -VERSION=1.7.5 +VERSION=1.8.5 ROOT=/home/will ICON=chart-671.png SHELL=fish diff --git a/README.md b/README.md index 5e41d2a..ae18633 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ make test ## 📚 Documentation - **[Features Guide](docs/FEATURES.md)** - Complete feature documentation +- **[Export System](docs/EXPORT_SYSTEM.md)** - Data export functionality and formats - **[Development Guide](docs/DEVELOPMENT.md)** - Testing, development, and architecture - **[Changelog](docs/CHANGELOG.md)** - Version history and feature evolution - **[Quick Reference](#quick-reference)** - Common commands and shortcuts @@ -226,6 +227,13 @@ On first run, the application will: - **Backward Compatibility**: Seamless upgrades without data loss - **Dynamic Columns**: Adapts to new medicines and pathologies +### 📋 Data Export System +- **Multiple Formats**: Export to JSON, XML, and PDF formats +- **Comprehensive Reports**: PDF exports with optional graph visualization +- **Metadata Inclusion**: Export includes date ranges, pathologies, and medicines +- **User-Friendly Interface**: Easy access through File menu with format selection +- **Data Portability**: Structured exports for analysis or backup purposes + For complete feature documentation, see **[docs/FEATURES.md](docs/FEATURES.md)**. ## Development diff --git a/docs/EXPORT_SYSTEM.md b/docs/EXPORT_SYSTEM.md new file mode 100644 index 0000000..6e1528c --- /dev/null +++ b/docs/EXPORT_SYSTEM.md @@ -0,0 +1,215 @@ +# TheChart Export System Documentation + +## Overview + +The TheChart application now includes a comprehensive data export system that allows users to export their medication tracking data and visualizations to multiple formats: + +- **JSON** - Structured data format with metadata +- **XML** - Hierarchical data format +- **PDF** - Formatted report with optional graph visualization + +## Features + +### Export Formats + +#### JSON Export +- Exports all CSV data to structured JSON format +- Includes metadata about the export (date, total entries, date range) +- Lists all pathologies and medicines being tracked +- Data is exported as an array of entry objects + +#### XML Export +- Exports data to hierarchical XML format +- Includes comprehensive metadata section +- All entries are properly structured with XML tags +- Column names are sanitized for valid XML element names + +#### PDF Export +- Creates a formatted report document +- Includes export metadata and summary information +- Optional graph visualization inclusion +- Data table with all entries +- Proper pagination and styling +- Notes are truncated for better table formatting + +### User Interface + +The export functionality is accessible through: +1. **File Menu** - "Export Data..." option in the main menu bar +2. **Export Window** - Modal dialog with export options +3. **Format Selection** - Radio buttons for JSON, XML, or PDF +4. **Graph Option** - Checkbox to include graph in PDF exports +5. **File Dialog** - Standard save dialog for choosing export location + +### Export Manager Architecture + +The export system consists of three main components: + +#### ExportManager Class (`src/export_manager.py`) +- Core export functionality +- Handles data transformation and file generation +- Integrates with existing data and graph managers +- Supports all three export formats + +#### ExportWindow Class (`src/export_window.py`) +- GUI interface for export operations +- Modal dialog with export options +- File save dialog integration +- Progress feedback and error handling + +#### Integration in MedTrackerApp (`src/main.py`) +- Export manager initialization +- Menu integration +- Seamless integration with existing managers + +## Technical Implementation + +### Dependencies Added +- `reportlab` - PDF generation library +- `lxml` - XML processing (added for future enhancements) +- `charset-normalizer` - Character encoding support + +### Data Flow +1. User selects export format and options +2. ExportManager loads data from DataManager +3. Data is transformed according to selected format +4. Graph image is optionally generated for PDF +5. Output file is created and saved +6. User receives success/failure feedback + +### Error Handling +- Graceful handling of missing data +- File system error management +- User-friendly error messages +- Logging of export operations + +## Usage Examples + +### Basic Export Process +1. Open TheChart application +2. Go to File → Export Data... +3. Select desired format (JSON/XML/PDF) +4. For PDF: choose whether to include graph +5. Click "Export..." button +6. Choose save location and filename +7. Confirm successful export + +### Export File Examples + +#### JSON Structure +```json +{ + "metadata": { + "export_date": "2025-08-02T09:03:22.580489", + "total_entries": 32, + "date_range": { + "start": "07/02/2025", + "end": "08/02/2025" + }, + "pathologies": ["depression", "anxiety", "sleep", "appetite"], + "medicines": ["bupropion", "hydroxyzine", "gabapentin", "propranolol", "quetiapine"] + }, + "entries": [ + { + "date": "07/02/2025", + "depression": 8, + "anxiety": 5, + "sleep": 3, + "appetite": 1, + "bupropion": 0, + "bupropion_doses": "", + "note": "Starting medication tracking" + } + ] +} +``` + +#### XML Structure +```xml + + + + 2025-08-02T09:03:22.613013 + 32 + + 07/02/2025 + 08/02/2025 + + + + + 07/02/2025 + 8 + 5 + Starting medication tracking + + + +``` + +## Testing + +### Automated Tests +- Export functionality is tested through `simple_export_test.py` +- Creates sample exports in all three formats +- Validates file creation and basic content structure + +### Manual Testing +- GUI testing available through `test_export_gui.py` +- Opens export window for interactive testing +- Allows testing of all user interface components + +### Test Files Location +Exported test files are created in the `test_exports/` directory: +- `export.json` - JSON format export +- `export.xml` - XML format export +- `export.csv` - CSV format copy +- `test_export.pdf` - PDF format with graph + +## File Locations + +### Source Files +- `src/export_manager.py` - Core export functionality +- `src/export_window.py` - GUI export interface + +### Test Files +- `simple_export_test.py` - Basic export functionality test +- `test_export_gui.py` - GUI testing interface +- `scripts/test_export_functionality.py` - Comprehensive export tests + +### Dependencies +- Added to `requirements.txt` and managed by `uv` +- PDF generation requires `reportlab` +- XML processing enhanced with `lxml` + +## Future Enhancements + +Potential improvements for the export system: +1. **Additional Formats** - Excel, CSV with formatting +2. **Export Filtering** - Date range selection, specific pathologies/medicines +3. **Batch Exports** - Multiple formats at once +4. **Email Integration** - Direct email export +5. **Cloud Storage** - Export to cloud services +6. **Export Scheduling** - Automated periodic exports +7. **Advanced PDF Styling** - Charts, graphs, custom layouts + +## Troubleshooting + +### Common Issues +1. **No Data to Export** - Ensure CSV file has entries before exporting +2. **PDF Generation Fails** - Check ReportLab installation and permissions +3. **File Save Errors** - Verify write permissions to selected directory +4. **Large File Exports** - PDF exports may take longer for large datasets + +### Debugging +- Check application logs for detailed error messages +- Export operations are logged with DEBUG level information +- File system errors are captured and reported to user + +## Integration Notes + +The export system integrates seamlessly with existing TheChart functionality: +- Uses same data validation and loading mechanisms +- Respects existing pathology and medicine configurations +- Maintains data integrity and formatting consistency +- Follows existing logging and error handling patterns diff --git a/pyproject.toml b/pyproject.toml index 350fec6..a48e202 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,16 @@ [project] name = "thechart" -version = "1.7.5" +version = "1.8.5" description = "Chart to monitor your medication intake over time." readme = "README.md" requires-python = ">=3.13" dependencies = [ "colorlog>=6.9.0", "dotenv>=0.9.9", + "lxml>=6.0.0", "matplotlib>=3.10.3", "pandas>=2.3.1", + "reportlab>=4.4.3", "tk>=0.1.0", ] diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..89fafc3 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,61 @@ +# TheChart Scripts Directory + +This directory contains testing and utility scripts for TheChart application. + +## Scripts Overview + +### Testing Scripts + +#### `run_tests.py` +Main test runner for the application. +```bash +cd /home/will/Code/thechart +.venv/bin/python scripts/run_tests.py +``` + +#### `integration_test.py` +Comprehensive integration test for the export system. +- Tests all export formats (JSON, XML, PDF) +- Validates data integrity and file creation +- No GUI dependencies - safe for automated testing + +```bash +cd /home/will/Code/thechart +.venv/bin/python scripts/integration_test.py +``` + +### Feature Testing Scripts + +#### `test_note_saving.py` +Tests note saving and retrieval functionality. +- Validates note persistence in CSV files +- Tests special characters and formatting + +#### `test_update_entry.py` +Tests entry update functionality. +- Validates data modification operations +- Tests date validation and duplicate handling + +## Usage + +All scripts should be run from the project root directory: + +```bash +cd /home/will/Code/thechart +.venv/bin/python scripts/.py +``` + +## Test Data + +- Integration tests create temporary export files in `integration_test_exports/` (auto-cleaned) +- Test scripts use the main `thechart_data.csv` file unless specified otherwise +- No test data is committed to the repository + +## Development + +When adding new scripts: +1. Place them in this directory +2. Use the standard shebang: `#!/usr/bin/env python3` +3. Add proper docstrings and error handling +4. Update this README with script documentation +5. Follow the project's linting and formatting standards diff --git a/scripts/integration_test.py b/scripts/integration_test.py new file mode 100644 index 0000000..20ffe51 --- /dev/null +++ b/scripts/integration_test.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +""" +Integration test for TheChart export system +Tests the complete export workflow without GUI dependencies +""" + +import sys +from pathlib import Path + +# Add src to path +sys.path.insert(0, "src") + +from data_manager import DataManager +from export_manager import ExportManager +from init import logger +from medicine_manager import MedicineManager +from pathology_manager import PathologyManager + + +class MockGraphManager: + """Mock graph manager for testing.""" + + def __init__(self): + self.fig = None + + +def test_integration(): + """Test complete export system integration.""" + print("TheChart Export System Integration Test") + print("=" * 45) + + # 1. Initialize all managers + print("\n1. Initializing managers...") + try: + medicine_manager = MedicineManager(logger=logger) + pathology_manager = PathologyManager(logger=logger) + data_manager = DataManager( + "thechart_data.csv", logger, medicine_manager, pathology_manager + ) + + # Mock graph manager (no GUI dependencies) + graph_manager = MockGraphManager() + + export_manager = ExportManager( + data_manager, graph_manager, medicine_manager, pathology_manager, logger + ) + print(" ✓ All managers initialized successfully") + except Exception as e: + print(f" ✗ Manager initialization failed: {e}") + return False + + # 2. Check data availability + print("\n2. Checking data availability...") + try: + export_info = export_manager.get_export_info() + print(f" Total entries: {export_info['total_entries']}") + print(f" Has data: {export_info['has_data']}") + + if not export_info["has_data"]: + print(" ✗ No data available for export") + return False + + print( + f" Date range: {export_info['date_range']['start']} " + f"to {export_info['date_range']['end']}" + ) + print(f" Pathologies: {len(export_info['pathologies'])}") + print(f" Medicines: {len(export_info['medicines'])}") + print(" ✓ Data is available for export") + except Exception as e: + print(f" ✗ Data check failed: {e}") + return False + + # 3. Test all export formats + export_dir = Path("integration_test_exports") + export_dir.mkdir(exist_ok=True) + + formats_to_test = [ + ("JSON", "integration_test.json", export_manager.export_data_to_json), + ("XML", "integration_test.xml", export_manager.export_data_to_xml), + ( + "PDF", + "integration_test.pdf", + lambda path: export_manager.export_to_pdf(path, include_graph=False), + ), + ] + + results = [] + + for format_name, filename, export_func in formats_to_test: + print(f"\n3.{len(results) + 1}. Testing {format_name} export...") + try: + file_path = export_dir / filename + success = export_func(str(file_path)) + + if success and file_path.exists(): + file_size = file_path.stat().st_size + print( + f" ✓ {format_name} export successful: {filename} " + f"({file_size} bytes)" + ) + results.append(True) + else: + print(f" ✗ {format_name} export failed") + results.append(False) + + except Exception as e: + print(f" ✗ {format_name} export error: {e}") + results.append(False) + + # 4. Summary + print("\n4. Test Summary") + print(f" Total tests: {len(results)}") + print(f" Passed: {sum(results)}") + print(f" Failed: {len(results) - sum(results)}") + + if all(results): + print(" ✓ All export formats working correctly!") + print(f" Check '{export_dir}' directory for exported files.") + return True + else: + print(" ✗ Some export formats failed") + return False + + +if __name__ == "__main__": + success = test_integration() + sys.exit(0 if success else 1) diff --git a/src/export_manager.py b/src/export_manager.py new file mode 100644 index 0000000..5f32f72 --- /dev/null +++ b/src/export_manager.py @@ -0,0 +1,385 @@ +""" +Export Manager for TheChart Application + +Handles exporting data and graphs to various formats: +- CSV data to JSON, XML +- Graphs to PDF (with data tables) +""" + +import contextlib +import json +import logging +import os +from datetime import datetime +from pathlib import Path +from typing import Any +from xml.dom import minidom +from xml.etree.ElementTree import Element, SubElement, tostring + +import pandas as pd +from reportlab.lib import colors +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet +from reportlab.lib.units import inch +from reportlab.platypus import ( + Image, + Paragraph, + SimpleDocTemplate, + Spacer, + Table, + TableStyle, +) + +from data_manager import DataManager +from graph_manager import GraphManager +from medicine_manager import MedicineManager +from pathology_manager import PathologyManager + + +class ExportManager: + """Handle data and graph export operations.""" + + def __init__( + self, + data_manager: DataManager, + graph_manager: GraphManager, + medicine_manager: MedicineManager, + pathology_manager: PathologyManager, + logger: logging.Logger, + ) -> None: + self.data_manager = data_manager + self.graph_manager = graph_manager + self.medicine_manager = medicine_manager + self.pathology_manager = pathology_manager + self.logger = logger + + def export_data_to_json(self, export_path: str) -> bool: + """Export CSV data to JSON format.""" + try: + df = self.data_manager.load_data() + if df.empty: + self.logger.warning("No data to export") + return False + + # Convert DataFrame to dictionary with better structure + export_data = { + "metadata": { + "export_date": datetime.now().isoformat(), + "total_entries": len(df), + "date_range": { + "start": df["date"].min() if not df.empty else None, + "end": df["date"].max() if not df.empty else None, + }, + "pathologies": list(self.pathology_manager.get_pathology_keys()), + "medicines": list(self.medicine_manager.get_medicine_keys()), + }, + "entries": df.to_dict(orient="records"), + } + + with open(export_path, "w", encoding="utf-8") as f: + json.dump(export_data, f, indent=2, ensure_ascii=False) + + self.logger.info(f"Data exported to JSON: {export_path}") + return True + + except Exception as e: + self.logger.error(f"Error exporting to JSON: {str(e)}") + return False + + def export_data_to_xml(self, export_path: str) -> bool: + """Export CSV data to XML format.""" + try: + df = self.data_manager.load_data() + if df.empty: + self.logger.warning("No data to export") + return False + + # Create root element + root = Element("thechart_data") + + # Add metadata + metadata = SubElement(root, "metadata") + SubElement(metadata, "export_date").text = datetime.now().isoformat() + SubElement(metadata, "total_entries").text = str(len(df)) + + # Date range + date_range = SubElement(metadata, "date_range") + SubElement(date_range, "start").text = ( + df["date"].min() if not df.empty else "" + ) + SubElement(date_range, "end").text = ( + df["date"].max() if not df.empty else "" + ) + + # Pathologies + pathologies = SubElement(metadata, "pathologies") + for pathology in self.pathology_manager.get_pathology_keys(): + SubElement(pathologies, "pathology").text = pathology + + # Medicines + medicines = SubElement(metadata, "medicines") + for medicine in self.medicine_manager.get_medicine_keys(): + SubElement(medicines, "medicine").text = medicine + + # Add entries + entries = SubElement(root, "entries") + for _, row in df.iterrows(): + entry = SubElement(entries, "entry") + for column, value in row.items(): + elem = SubElement(entry, column.replace(" ", "_")) + elem.text = str(value) if pd.notna(value) else "" + + # Pretty print XML + rough_string = tostring(root, "utf-8") + reparsed = minidom.parseString(rough_string) + pretty_xml = reparsed.toprettyxml(indent=" ") + + with open(export_path, "w", encoding="utf-8") as f: + f.write(pretty_xml) + + self.logger.info(f"Data exported to XML: {export_path}") + return True + + except Exception as e: + self.logger.error(f"Error exporting to XML: {str(e)}") + return False + + def _save_graph_as_image(self, temp_dir: Path) -> str | None: + """Save current graph as temporary image for PDF inclusion.""" + try: + # Check if graph manager exists + if self.graph_manager is None: + self.logger.warning("No graph manager available for export") + return None + + # Check if graph manager and figure exist + if not hasattr(self.graph_manager, "fig") or self.graph_manager.fig is None: + self.logger.warning("No graph figure available for export") + return None + + # Ensure graph is up to date with current data + df = self.data_manager.load_data() + if not df.empty: + self.graph_manager.update_graph(df) + else: + self.logger.warning("No data available to update graph for export") + return None + + # Ensure temp directory exists + temp_dir.mkdir(parents=True, exist_ok=True) + temp_image_path = temp_dir / "graph.png" + + # Save the current figure + self.graph_manager.fig.savefig( + str(temp_image_path), + dpi=150, + bbox_inches="tight", + facecolor="white", + edgecolor="none", + ) + + # Verify the file was actually created + if not temp_image_path.exists(): + self.logger.error( + f"Graph image file was not created: {temp_image_path}" + ) + return None + + self.logger.info(f"Graph image saved successfully: {temp_image_path}") + return str(temp_image_path) + + except Exception as e: + self.logger.error(f"Error saving graph image: {str(e)}") + return None + + def export_to_pdf(self, export_path: str, include_graph: bool = True) -> bool: + """Export data and optionally graph to PDF format.""" + try: + df = self.data_manager.load_data() + + # Create PDF document + doc = SimpleDocTemplate( + export_path, + pagesize=A4, + rightMargin=72, + leftMargin=72, + topMargin=72, + bottomMargin=18, + ) + + # Get styles + styles = getSampleStyleSheet() + title_style = ParagraphStyle( + "CustomTitle", + parent=styles["Heading1"], + fontSize=18, + spaceAfter=30, + textColor=colors.darkblue, + ) + + story = [] + + # Title + story.append(Paragraph("TheChart - Medication Tracker Export", title_style)) + story.append(Spacer(1, 20)) + + # Export metadata + export_info = [ + f"Export Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + f"Total Entries: {len(df) if not df.empty else 0}", + ] + + if not df.empty: + export_info.extend( + [ + f"Date Range: {df['date'].min()} to {df['date'].max()}", + ( + "Pathologies: " + + ", ".join(self.pathology_manager.get_pathology_keys()) + ), + ( + "Medicines: " + + ", ".join(self.medicine_manager.get_medicine_keys()) + ), + ] + ) + + for info in export_info: + story.append(Paragraph(info, styles["Normal"])) + + story.append(Spacer(1, 20)) + + # Include graph if requested and available + if include_graph: + temp_dir = Path(export_path).parent / "temp_export" + + try: + graph_path = self._save_graph_as_image(temp_dir) + if graph_path and os.path.exists(graph_path): + story.append( + Paragraph("Data Visualization", styles["Heading2"]) + ) + story.append(Spacer(1, 10)) + + # Add graph image + img = Image(graph_path, width=6 * inch, height=3.6 * inch) + story.append(img) + story.append(Spacer(1, 20)) + + # Clean up temp image + os.remove(graph_path) + else: + # Graph not available, add a note instead + story.append( + Paragraph("Data Visualization", styles["Heading2"]) + ) + story.append(Spacer(1, 10)) + story.append( + Paragraph( + "Graph not available - no data to visualize or graph " + "not generated yet.", + styles["Normal"], + ) + ) + story.append(Spacer(1, 20)) + + except Exception as e: + self.logger.error(f"Error including graph in PDF: {str(e)}") + # Add error note instead of failing completely + story.append(Paragraph("Data Visualization", styles["Heading2"])) + story.append(Spacer(1, 10)) + story.append( + Paragraph( + f"Graph could not be included: {str(e)}", styles["Normal"] + ) + ) + story.append(Spacer(1, 20)) + + finally: + # Clean up temp directory + if temp_dir.exists(): + with contextlib.suppress(OSError): + temp_dir.rmdir() + + # Add data table if we have data + if not df.empty: + story.append(Paragraph("Data Table", styles["Heading2"])) + story.append(Spacer(1, 10)) + + # Prepare table data - limit columns for better PDF formatting + display_columns = ["date"] + for pathology_key in self.pathology_manager.get_pathology_keys(): + display_columns.append(pathology_key) + for medicine_key in self.medicine_manager.get_medicine_keys(): + display_columns.append(medicine_key) + display_columns.append("note") + + # Filter dataframe to display columns that exist + available_columns = [ + col for col in display_columns if col in df.columns + ] + display_df = df[available_columns].copy() + + # Truncate long notes for better table formatting + if "note" in display_df.columns: + display_df["note"] = display_df["note"].apply( + lambda x: (str(x)[:50] + "...") if len(str(x)) > 50 else str(x) + ) + + # Convert to table data + table_data = [available_columns] # Headers + for _, row in display_df.iterrows(): + table_data.append( + [str(val) if pd.notna(val) else "" for val in row] + ) + + # Create table with styling + table = Table(table_data, repeatRows=1) + table.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), colors.grey), + ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke), + ("ALIGN", (0, 0), (-1, -1), "CENTER"), + ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), + ("FONTSIZE", (0, 0), (-1, 0), 10), + ("BOTTOMPADDING", (0, 0), (-1, 0), 12), + ("BACKGROUND", (0, 1), (-1, -1), colors.beige), + ("FONTNAME", (0, 1), (-1, -1), "Helvetica"), + ("FONTSIZE", (0, 1), (-1, -1), 8), + ("GRID", (0, 0), (-1, -1), 1, colors.black), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ] + ) + ) + + story.append(table) + else: + story.append( + Paragraph("No data available to export.", styles["Normal"]) + ) + + # Build PDF + doc.build(story) + + self.logger.info(f"Data exported to PDF: {export_path}") + return True + + except Exception as e: + self.logger.error(f"Error exporting to PDF: {str(e)}") + return False + + def get_export_info(self) -> dict[str, Any]: + """Get information about available data for export.""" + df = self.data_manager.load_data() + + return { + "total_entries": len(df) if not df.empty else 0, + "date_range": { + "start": df["date"].min() if not df.empty else None, + "end": df["date"].max() if not df.empty else None, + }, + "pathologies": list(self.pathology_manager.get_pathology_keys()), + "medicines": list(self.medicine_manager.get_medicine_keys()), + "has_data": not df.empty, + } diff --git a/src/export_window.py b/src/export_window.py new file mode 100644 index 0000000..cb704e9 --- /dev/null +++ b/src/export_window.py @@ -0,0 +1,247 @@ +""" +Export Window for TheChart Application + +Provides a GUI interface for exporting data and graphs to various formats. +""" + +import tkinter as tk +from pathlib import Path +from tkinter import filedialog, messagebox, ttk + +from export_manager import ExportManager + + +class ExportWindow: + """Export window for data and graph export functionality.""" + + def __init__(self, parent: tk.Tk, export_manager: ExportManager) -> None: + self.parent = parent + self.export_manager = export_manager + + # Create the export window + self.window = tk.Toplevel(parent) + self.window.title("Export Data") + self.window.geometry("500x450") # Made taller to ensure buttons are visible + self.window.resizable(False, False) + + # Center the window + self._center_window() + + # Make window modal + self.window.transient(parent) + self.window.grab_set() + + # Setup the UI + self._setup_ui() + + def _center_window(self) -> None: + """Center the export window on the parent window.""" + self.window.update_idletasks() + + # Get window dimensions + width = self.window.winfo_width() + height = self.window.winfo_height() + + # Get parent window position and size + parent_x = self.parent.winfo_rootx() + parent_y = self.parent.winfo_rooty() + parent_width = self.parent.winfo_width() + parent_height = self.parent.winfo_height() + + # Calculate position to center on parent + x = parent_x + (parent_width // 2) - (width // 2) + y = parent_y + (parent_height // 2) - (height // 2) + + self.window.geometry(f"{width}x{height}+{x}+{y}") + + def _setup_ui(self) -> None: + """Setup the export window UI.""" + # Main frame + main_frame = ttk.Frame(self.window, padding="15") + main_frame.pack(fill=tk.BOTH, expand=True) + + # Title + title_label = ttk.Label( + main_frame, text="Export Data & Graphs", font=("Arial", 14, "bold") + ) + title_label.pack(pady=(0, 15)) + + # Create scrollable content area for the main content + content_frame = ttk.Frame(main_frame) + content_frame.pack(fill=tk.BOTH, expand=True) + + # Export info section + self._create_info_section(content_frame) + + # Export options section + self._create_options_section(content_frame) + + # Buttons section - always at the bottom + self._create_buttons_section(main_frame) + + def _create_info_section(self, parent: ttk.Frame) -> None: + """Create the data information section.""" + info_frame = ttk.LabelFrame(parent, text="Data Summary", padding="10") + info_frame.pack(fill=tk.X, pady=(0, 20)) + + # Get export info + export_info = self.export_manager.get_export_info() + + # Display information + if export_info["has_data"]: + info_text = f"""Total Entries: {export_info["total_entries"]} +Date Range: {export_info["date_range"]["start"]} to {export_info["date_range"]["end"]} +Pathologies: {", ".join(export_info["pathologies"])} +Medicines: {", ".join(export_info["medicines"])}""" + else: + info_text = "No data available for export." + + info_label = ttk.Label(info_frame, text=info_text, justify=tk.LEFT) + info_label.pack(anchor=tk.W) + + def _create_options_section(self, parent: ttk.Frame) -> None: + """Create the export options section.""" + options_frame = ttk.LabelFrame(parent, text="Export Options", padding="10") + options_frame.pack(fill=tk.X, pady=(0, 20)) + + # Include graph option (for PDF export) + self.include_graph_var = tk.BooleanVar(value=True) + graph_check = ttk.Checkbutton( + options_frame, + text="Include graph in PDF export", + variable=self.include_graph_var, + ) + graph_check.pack(anchor=tk.W, pady=(0, 10)) + + # Format selection + format_label = ttk.Label(options_frame, text="Export Format:") + format_label.pack(anchor=tk.W) + + self.format_var = tk.StringVar(value="JSON") + formats = ["JSON", "XML", "PDF"] + + for fmt in formats: + radio = ttk.Radiobutton( + options_frame, text=fmt, variable=self.format_var, value=fmt + ) + radio.pack(anchor=tk.W, padx=(20, 0)) + + def _create_buttons_section(self, parent: ttk.Frame) -> None: + """Create the buttons section.""" + # Add a separator for visual clarity + separator = ttk.Separator(parent, orient="horizontal") + separator.pack(fill=tk.X, pady=(10, 10)) + + button_frame = ttk.Frame(parent) + button_frame.pack(fill=tk.X, pady=(0, 10)) + + # Export button with more prominent styling + export_btn = ttk.Button( + button_frame, text="Export...", command=self._handle_export + ) + export_btn.pack(side=tk.LEFT, padx=(10, 10), pady=5) + + # Cancel button + cancel_btn = ttk.Button( + button_frame, text="Cancel", command=self.window.destroy + ) + cancel_btn.pack(side=tk.RIGHT, padx=(10, 10), pady=5) + + def _handle_export(self) -> None: + """Handle the export button click.""" + # Check if we have data to export + export_info = self.export_manager.get_export_info() + if not export_info["has_data"]: + messagebox.showwarning( + "No Data", "There is no data available to export.", parent=self.window + ) + return + + # Get selected format + selected_format = self.format_var.get() + + # Define file types for dialog + file_types = { + "JSON": [("JSON files", "*.json"), ("All files", "*.*")], + "XML": [("XML files", "*.xml"), ("All files", "*.*")], + "PDF": [("PDF files", "*.pdf"), ("All files", "*.*")], + } + + # Default filename + default_name = f"thechart_export.{selected_format.lower()}" + + # Show save dialog + filename = filedialog.asksaveasfilename( + parent=self.window, + title=f"Export as {selected_format}", + defaultextension=f".{selected_format.lower()}", + filetypes=file_types[selected_format], + initialfile=default_name, + ) + + if not filename: + return + + # Perform export based on selected format + success = False + try: + if selected_format == "JSON": + success = self.export_manager.export_data_to_json(filename) + elif selected_format == "XML": + success = self.export_manager.export_data_to_xml(filename) + elif selected_format == "PDF": + include_graph = self.include_graph_var.get() + success = self.export_manager.export_to_pdf( + filename, include_graph=include_graph + ) + + if success: + messagebox.showinfo( + "Export Successful", + f"Data exported successfully to:\n{filename}", + parent=self.window, + ) + # Ask if user wants to open the file location + if messagebox.askyesno( + "Open Location", + "Would you like to open the file location?", + parent=self.window, + ): + self._open_file_location(filename) + + self.window.destroy() + else: + messagebox.showerror( + "Export Failed", + f"Failed to export data as {selected_format}. " + "Please check the logs for more details.", + parent=self.window, + ) + + except Exception as e: + messagebox.showerror( + "Export Error", + f"An error occurred during export:\n{str(e)}", + parent=self.window, + ) + + def _open_file_location(self, filepath: str) -> None: + """Open the file location in the system file manager.""" + try: + file_path = Path(filepath) + directory = file_path.parent + + # Use system-specific command to open file manager + import subprocess + import sys + + if sys.platform == "win32": + subprocess.run(["explorer", str(directory)], check=False) + elif sys.platform == "darwin": + subprocess.run(["open", str(directory)], check=False) + else: # Linux and other Unix-like systems + subprocess.run(["xdg-open", str(directory)], check=False) + + except Exception: + # If opening file location fails, just ignore silently + pass diff --git a/src/main.py b/src/main.py index 5b19bf1..b049f8d 100644 --- a/src/main.py +++ b/src/main.py @@ -9,6 +9,8 @@ import pandas as pd from constants import LOG_CLEAR, LOG_LEVEL, LOG_PATH from data_manager import DataManager +from export_manager import ExportManager +from export_window import ExportWindow from graph_manager import GraphManager from init import logger from medicine_management_window import MedicineManagementWindow @@ -117,6 +119,15 @@ class MedTrackerApp: graph_frame, self.medicine_manager, self.pathology_manager ) + # Initialize export manager + self.export_manager: ExportManager = ExportManager( + self.data_manager, + self.graph_manager, + self.medicine_manager, + self.pathology_manager, + logger, + ) + # --- Create Input Frame --- input_ui: dict[str, Any] = self.ui_manager.create_input_frame(main_frame) self.input_frame: ttk.Frame = input_ui["frame"] @@ -152,6 +163,13 @@ class MedTrackerApp: menubar = tk.Menu(self.root) self.root.config(menu=menubar) + # File menu + file_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="File", menu=file_menu) + file_menu.add_command(label="Export Data...", command=self._open_export_window) + file_menu.add_separator() + file_menu.add_command(label="Exit", command=self.handle_window_closing) + # Tools menu tools_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Tools", menu=tools_menu) @@ -162,6 +180,10 @@ class MedTrackerApp: label="Manage Medicines...", command=self._open_medicine_manager ) + def _open_export_window(self) -> None: + """Open the export window.""" + ExportWindow(self.root, self.export_manager) + def _open_pathology_manager(self) -> None: """Open the pathology management window.""" PathologyManagementWindow( diff --git a/uv.lock b/uv.lock index f029ef5..d51a22b 100644 --- a/uv.lock +++ b/uv.lock @@ -20,6 +20,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -258,6 +280,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" }, ] +[[package]] +name = "lxml" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" }, + { url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" }, + { url = "https://files.pythonhosted.org/packages/52/46/3572761efc1bd45fcafb44a63b3b0feeb5b3f0066886821e94b0254f9253/lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81", size = 4947559, upload-time = "2025-06-28T18:47:31.091Z" }, + { url = "https://files.pythonhosted.org/packages/94/8a/5e40de920e67c4f2eef9151097deb9b52d86c95762d8ee238134aff2125d/lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1", size = 5102143, upload-time = "2025-06-28T18:47:33.612Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload-time = "2025-06-26T16:26:47.503Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/cf03b412f3763d4ca23b25e70c96a74cfece64cec3addf1c4ec639586b13/lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a", size = 5645469, upload-time = "2025-07-03T19:19:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload-time = "2025-06-26T16:26:49.998Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload-time = "2025-06-26T16:26:52.564Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload-time = "2025-06-26T16:26:55.054Z" }, + { url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload-time = "2025-06-26T16:26:57.384Z" }, + { url = "https://files.pythonhosted.org/packages/06/d4/fd216f3cd6625022c25b336c7570d11f4a43adbaf0a56106d3d496f727a7/lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f", size = 5662049, upload-time = "2025-07-03T19:19:16.409Z" }, + { url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload-time = "2025-06-26T16:27:00.031Z" }, + { url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload-time = "2025-06-26T16:27:04.251Z" }, + { url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" }, + { url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" }, +] + [[package]] name = "macholib" version = "1.16.3" @@ -653,6 +699,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] +[[package]] +name = "reportlab" +version = "4.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/83/3d44b873fa71ddc7d323c577fe4cfb61e05b34d14e64b6a232f9cfbff89d/reportlab-4.4.3.tar.gz", hash = "sha256:073b0975dab69536acd3251858e6b0524ed3e087e71f1d0d1895acb50acf9c7b", size = 3887532, upload-time = "2025-07-23T11:18:23.799Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/c8/aaf4e08679e7b1dc896ad30de0d0527f0fd55582c2e6deee4f2cc899bf9f/reportlab-4.4.3-py3-none-any.whl", hash = "sha256:df905dc5ec5ddaae91fc9cb3371af863311271d555236410954961c5ee6ee1b5", size = 1953896, upload-time = "2025-07-23T11:18:20.572Z" }, +] + [[package]] name = "ruff" version = "0.12.5" @@ -698,13 +757,15 @@ wheels = [ [[package]] name = "thechart" -version = "1.7.5" +version = "1.8.5" source = { virtual = "." } dependencies = [ { name = "colorlog" }, { name = "dotenv" }, + { name = "lxml" }, { name = "matplotlib" }, { name = "pandas" }, + { name = "reportlab" }, { name = "tk" }, ] @@ -723,8 +784,10 @@ dev = [ requires-dist = [ { name = "colorlog", specifier = ">=6.9.0" }, { name = "dotenv", specifier = ">=0.9.9" }, + { name = "lxml", specifier = ">=6.0.0" }, { name = "matplotlib", specifier = ">=3.10.3" }, { name = "pandas", specifier = ">=2.3.1" }, + { name = "reportlab", specifier = ">=4.4.3" }, { name = "tk", specifier = ">=0.1.0" }, ]