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" },
]