Coverage for src/lexigram/admin/services/export/adapters/excel.py: 25%
48 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:44 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:44 +0800
1from __future__ import annotations
3from datetime import UTC
4import io
5from typing import TYPE_CHECKING, Any
7from lexigram.admin.services.export.sanitize import sanitize_cell_value
8from lexigram.admin.services.export.service import IExportBackend
10if TYPE_CHECKING:
11 from lexigram.admin.services.export.scheduler import ExportJob
13try:
14 import openpyxl # type: ignore[import-untyped]
15 from openpyxl.styles import ( # type: ignore[import-untyped]
16 Alignment,
17 Font,
18 PatternFill,
19 )
21 HAS_OPENPYXL = True
22except ImportError:
23 HAS_OPENPYXL = False
26class ExcelExportBackend(IExportBackend):
27 """Backend for Excel exports."""
29 async def generate_file(
30 self,
31 job: ExportJob,
32 data: list[dict[str, Any]],
33 storage: Any,
34 export_dir: str,
35 ) -> str:
36 """Export data to Excel format."""
37 if not HAS_OPENPYXL:
38 raise ImportError("openpyxl is required for Excel export")
40 from datetime import datetime
42 timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
43 filename = f"{job.resource_name}_export_{timestamp}.xlsx"
44 file_path = f"{export_dir}/{filename}"
46 workbook = openpyxl.Workbook()
47 worksheet = workbook.active
48 worksheet.title = "Export"
50 if data:
51 # Determine columns
52 fieldnames = job.columns or list(data[0].keys())
54 # Write headers
55 for col_num, header in enumerate(fieldnames, 1):
56 cell = worksheet.cell(row=1, column=col_num, value=header)
57 cell.font = Font(bold=True)
58 cell.fill = PatternFill(
59 start_color="FFE6E6FA",
60 end_color="FFE6E6FA",
61 fill_type="solid",
62 )
64 # Write data
65 for row_num, row in enumerate(data, 2):
66 for col_num, field in enumerate(fieldnames, 1):
67 value = sanitize_cell_value(row.get(field, ""))
68 cell = worksheet.cell(row=row_num, column=col_num, value=value)
70 # Basic formatting
71 if isinstance(value, (int, float)):
72 cell.alignment = Alignment(horizontal="right")
73 elif isinstance(value, datetime):
74 cell.number_format = "YYYY-MM-DD HH:MM:SS"
76 # Auto-adjust column widths
77 for col_num, field in enumerate(fieldnames, 1):
78 column_letter = openpyxl.utils.get_column_letter(col_num)
79 max_length = len(field) # Start with header length
81 for row in data[:100]: # Sample first 100 rows
82 value = str(row.get(field, ""))
83 max_length = max(max_length, len(value))
85 worksheet.column_dimensions[column_letter].width = min(
86 max_length + 2,
87 50,
88 )
90 # Save to storage
91 buffer = io.BytesIO()
92 workbook.save(buffer)
93 await storage.upload(
94 file_path,
95 buffer.getvalue(),
96 content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
97 )
99 return file_path