zai-telegram-bot / file_generators.py
Ejdjdososs's picture
Upload file_generators.py with huggingface_hub
d6f83ee verified
Raw
History Blame Contribute Delete
9.43 kB
"""
وحدة توليد الملفات والمخططات
- PDF عبر ReportLab
- Word عبر python-docx
- Excel عبر openpyxl
- مخططات عبر matplotlib
كل الملفات تُحفظ مؤقتاً وتُرسل كمرفقات
"""
import asyncio
import io
import logging
import os
import tempfile
import uuid
from typing import List, Optional, Tuple
logger = logging.getLogger(__name__)
# ضبط matplotlib لـ headless
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
# محاولة تسجيل خطوط عربية إن وجدت
try:
for fp in [
"/usr/share/fonts/truetype/chinese/NotoSansSC-Regular.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/lxgw-wenkai/LXGWWenKai-Regular.ttf",
]:
if os.path.exists(fp):
fm.fontManager.addfont(fp)
plt.rcParams["font.sans-serif"] = ["Noto Sans SC", "DejaVu Sans", "Liberation Sans"]
plt.rcParams["axes.unicode_minus"] = False
except Exception as e:
logger.warning(f"Font setup warning: {e}")
# ===================== PDF =====================
def create_pdf(content: str, title: str = "مستند") -> str:
"""إنشاء PDF من نص - يعيد مسار الملف"""
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.units import cm
from reportlab.lib.enums import TA_RIGHT
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
except ImportError as e:
raise RuntimeError("reportlab غير مثبت") from e
# محاولة تسجيل خط عربي
arabic_font_name = "Helvetica"
try:
arabic_font_path = None
for p in [
"/usr/share/fonts/truetype/lxgw-wenkai/LXGWWenKai-Regular.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
]:
if os.path.exists(p):
arabic_font_path = p
break
if arabic_font_path:
pdfmetrics.registerFont(TTFont("ArabicFont", arabic_font_path))
arabic_font_name = "ArabicFont"
except Exception:
pass
output_path = os.path.join(tempfile.gettempdir(), f"{title[:30]}_{uuid.uuid4().hex[:6]}.pdf")
doc = SimpleDocTemplate(
output_path, pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=2*cm, bottomMargin=2*cm,
)
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
"ArabicTitle", parent=styles["Title"],
fontName=arabic_font_name, fontSize=20, alignment=TA_RIGHT,
spaceAfter=20,
)
body_style = ParagraphStyle(
"ArabicBody", parent=styles["Normal"],
fontName=arabic_font_name, fontSize=12, alignment=TA_RIGHT,
leading=18, spaceAfter=10,
)
story = []
story.append(Paragraph(title, title_style))
story.append(Spacer(1, 1*cm))
# تقسيم المحتوى لفقرات
for line in content.split("\n"):
if line.strip():
# escape XML
safe = line.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
story.append(Paragraph(safe, body_style))
else:
story.append(Spacer(1, 0.3*cm))
doc.build(story)
return output_path
# ===================== DOCX =====================
def create_docx(content: str, title: str = "مستند") -> str:
"""إنشاء Word DOCX من نص"""
try:
from docx import Document
from docx.shared import Pt, Inches, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
except ImportError as e:
raise RuntimeError("python-docx غير مثبت") from e
output_path = os.path.join(tempfile.gettempdir(), f"{title[:30]}_{uuid.uuid4().hex[:6]}.docx")
doc = Document()
# عنوان
title_para = doc.add_heading(title, level=1)
title_para.alignment = WD_ALIGN_PARAGRAPH.RIGHT
# محتوى
for line in content.split("\n"):
if line.strip():
p = doc.add_paragraph(line)
p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
for run in p.runs:
run.font.size = Pt(12)
doc.save(output_path)
return output_path
# ===================== XLSX =====================
def create_xlsx(data: List[List], sheet_name: str = "Sheet1", title: str = "بيانات") -> str:
"""إنشاء Excel من بيانات (list of lists)"""
try:
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
except ImportError as e:
raise RuntimeError("openpyxl غير مثبت") from e
output_path = os.path.join(tempfile.gettempdir(), f"{title[:30]}_{uuid.uuid4().hex[:6]}.xlsx")
wb = Workbook()
ws = wb.active
ws.title = sheet_name[:31] # حد Excel لاسم الورقة
if not data:
ws.append(["لا توجد بيانات"])
else:
for row in data:
ws.append(row)
# تنسيق الصف الأول (headers)
if len(data) > 0:
header_font = Font(bold=True, color="FFFFFF", size=11)
header_fill = PatternFill("solid", fgColor="4F81BD")
thin_border = Border(
left=Side(style="thin"), right=Side(style="thin"),
top=Side(style="thin"), bottom=Side(style="thin"),
)
for cell in ws[1]:
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal="center", vertical="center")
cell.border = thin_border
# ضبط عرض الأعمدة تلقائياً
for col in ws.columns:
max_len = 0
col_letter = get_column_letter(col[0].column)
for cell in col:
try:
cell_len = len(str(cell.value)) if cell.value else 0
if cell_len > max_len:
max_len = cell_len
except Exception:
pass
ws.column_dimensions[col_letter].width = min(max_len + 2, 50)
wb.save(output_path)
return output_path
# ===================== Charts (matplotlib) =====================
def create_chart(
chart_type: str,
data: dict,
title: str = "مخطط",
xlabel: str = "",
ylabel: str = "",
) -> str:
"""
إنشاء مخطط بصورة PNG.
chart_type: bar, line, pie, scatter, histogram
data: dict حسب نوع المخطط
- bar/line: {"labels": [...], "values": [...]}
- pie: {"labels": [...], "values": [...]}
- scatter: {"x": [...], "y": [...]}
- histogram: {"values": [...]}
"""
output_path = os.path.join(tempfile.gettempdir(), f"chart_{uuid.uuid4().hex[:6]}.png")
fig, ax = plt.subplots(figsize=(10, 6), constrained_layout=True)
try:
if chart_type == "bar":
labels = data.get("labels", [])
values = data.get("values", [])
ax.bar(labels, values, color="steelblue", edgecolor="navy")
elif chart_type == "line":
labels = data.get("labels", [])
values = data.get("values", [])
ax.plot(labels, values, marker="o", color="crimson", linewidth=2)
ax.grid(True, alpha=0.3)
elif chart_type == "pie":
labels = data.get("labels", [])
values = data.get("values", [])
ax.pie(values, labels=labels, autopct="%1.1f%%", startangle=90)
ax.axis("equal")
elif chart_type == "scatter":
x = data.get("x", [])
y = data.get("y", [])
ax.scatter(x, y, color="darkorange", alpha=0.7, edgecolors="black")
ax.grid(True, alpha=0.3)
elif chart_type == "histogram":
values = data.get("values", [])
ax.hist(values, bins=20, color="mediumpurple", edgecolor="indigo")
else:
raise ValueError(f"نوع مخطط غير معروف: {chart_type}")
if title:
ax.set_title(title, fontsize=14, fontweight="bold")
if xlabel:
ax.set_xlabel(xlabel, fontsize=11)
if ylabel:
ax.set_ylabel(ylabel, fontsize=11)
# تدوير labels المحور السيني إن طالت
if chart_type in ("bar", "line") and data.get("labels"):
if len(data["labels"]) > 5:
plt.setp(ax.get_xticklabels(), rotation=30, ha="right")
fig.savefig(output_path, dpi=120)
finally:
plt.close(fig)
return output_path
# ===================== مساعد =====================
def cleanup_file(path: str):
"""حذف ملف مؤقت بأمان"""
try:
if path and os.path.exists(path):
os.remove(path)
except Exception as e:
logger.warning(f"Could not delete {path}: {e}")
async def run_in_executor(func, *args, **kwargs):
"""تشغيل دالة متزامنة في thread pool"""
return await asyncio.to_thread(func, *args, **kwargs)