Spaces:
Runtime error
Runtime error
import os | |
import logging | |
import pandas as pd | |
import re | |
from datetime import datetime | |
from pathlib import Path | |
import markdown | |
import matplotlib.pyplot as plt | |
import base64 | |
from io import BytesIO | |
logger = logging.getLogger(__name__) | |
class ReportGenerator: | |
""" | |
Клас для генерації звітів на основі аналізу даних Jira | |
""" | |
def __init__(self, df, stats=None, inactive_issues=None, ai_analysis=None): | |
""" | |
Ініціалізація генератора звітів. | |
Args: | |
df (pandas.DataFrame): DataFrame з даними Jira | |
stats (dict): Словник зі статистикою (або None) | |
inactive_issues (dict): Дані про неактивні тікети (або None) | |
ai_analysis (str): Текст AI аналізу (або None) | |
""" | |
self.df = df | |
self.stats = stats | |
self.inactive_issues = inactive_issues | |
self.ai_analysis = ai_analysis | |
def create_markdown_report(self, inactive_days=14): | |
""" | |
Створення звіту у форматі Markdown. | |
Args: | |
inactive_days (int): Кількість днів для визначення неактивних тікетів | |
Returns: | |
str: Текст звіту у форматі Markdown | |
""" | |
try: | |
report = [] | |
# Заголовок звіту | |
report.append("# Звіт аналізу Jira") | |
report.append(f"*Створено: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*") | |
# Загальна статистика | |
report.append("\n## Загальна статистика") | |
if self.stats and 'total_tickets' in self.stats: | |
report.append(f"**Загальна кількість тікетів:** {self.stats['total_tickets']}") | |
else: | |
report.append(f"**Загальна кількість тікетів:** {len(self.df)}") | |
# Статистика за статусами | |
if self.stats and 'status_counts' in self.stats and self.stats['status_counts']: | |
report.append("\n### Статуси тікетів") | |
for status, count in self.stats['status_counts'].items(): | |
percentage = count / self.stats['total_tickets'] * 100 if self.stats['total_tickets'] > 0 else 0 | |
report.append(f"- **{status}:** {count} ({percentage:.1f}%)") | |
elif 'Status' in self.df.columns: | |
status_counts = self.df['Status'].value_counts() | |
report.append("\n### Статуси тікетів") | |
for status, count in status_counts.items(): | |
percentage = count / len(self.df) * 100 if len(self.df) > 0 else 0 | |
report.append(f"- **{status}:** {count} ({percentage:.1f}%)") | |
# Статистика за типами | |
if self.stats and 'type_counts' in self.stats and self.stats['type_counts']: | |
report.append("\n### Типи тікетів") | |
for type_name, count in self.stats['type_counts'].items(): | |
percentage = count / self.stats['total_tickets'] * 100 if self.stats['total_tickets'] > 0 else 0 | |
report.append(f"- **{type_name}:** {count} ({percentage:.1f}%)") | |
elif 'Issue Type' in self.df.columns: | |
type_counts = self.df['Issue Type'].value_counts() | |
report.append("\n### Типи тікетів") | |
for type_name, count in type_counts.items(): | |
percentage = count / len(self.df) * 100 if len(self.df) > 0 else 0 | |
report.append(f"- **{type_name}:** {count} ({percentage:.1f}%)") | |
# Статистика за пріоритетами | |
if self.stats and 'priority_counts' in self.stats and self.stats['priority_counts']: | |
report.append("\n### Пріоритети тікетів") | |
for priority, count in self.stats['priority_counts'].items(): | |
percentage = count / self.stats['total_tickets'] * 100 if self.stats['total_tickets'] > 0 else 0 | |
report.append(f"- **{priority}:** {count} ({percentage:.1f}%)") | |
elif 'Priority' in self.df.columns: | |
priority_counts = self.df['Priority'].value_counts() | |
report.append("\n### Пріоритети тікетів") | |
for priority, count in priority_counts.items(): | |
percentage = count / len(self.df) * 100 if len(self.df) > 0 else 0 | |
report.append(f"- **{priority}:** {count} ({percentage:.1f}%)") | |
# Аналіз часових показників | |
if 'Created' in self.df.columns and pd.api.types.is_datetime64_dtype(self.df['Created']): | |
report.append("\n## Часові показники") | |
min_date = self.df['Created'].min() | |
max_date = self.df['Created'].max() | |
report.append(f"**Період створення тікетів:** з {min_date.strftime('%Y-%m-%d')} по {max_date.strftime('%Y-%m-%d')}") | |
# Тікети за останній тиждень | |
last_week = (datetime.now() - pd.Timedelta(days=7)) | |
recent_tickets = self.df[self.df['Created'] >= last_week] | |
report.append(f"**Тікети, створені за останній тиждень:** {len(recent_tickets)}") | |
# Неактивні тікети | |
if self.inactive_issues: | |
report.append(f"\n## Неактивні тікети (>{inactive_days} днів)") | |
total_inactive = self.inactive_issues.get('total_count', 0) | |
percentage = self.inactive_issues.get('percentage', 0) | |
report.append(f"**Загальна кількість неактивних тікетів:** {total_inactive} ({percentage:.1f}%)") | |
if 'by_status' in self.inactive_issues and self.inactive_issues['by_status']: | |
report.append("\n**Неактивні тікети за статусами:**") | |
for status, count in self.inactive_issues['by_status'].items(): | |
report.append(f"- **{status}:** {count}") | |
if 'top_inactive' in self.inactive_issues and self.inactive_issues['top_inactive']: | |
report.append("\n**Топ 5 найбільш неактивних тікетів:**") | |
for i, ticket in enumerate(self.inactive_issues['top_inactive']): | |
key = ticket.get('key', 'Невідомо') | |
summary = ticket.get('summary', 'Невідомо') | |
status = ticket.get('status', 'Невідомо') | |
days = ticket.get('days_inactive', 'Невідомо') | |
report.append(f"{i+1}. **{key}:** {summary}") | |
report.append(f" - Статус: {status}") | |
report.append(f" - Днів неактивності: {days}") | |
# AI Аналіз | |
if self.ai_analysis: | |
report.append("\n## AI Аналіз") | |
report.append(self.ai_analysis) | |
logger.info("Звіт успішно згенеровано у форматі Markdown") | |
return "\n".join(report) | |
except Exception as e: | |
logger.error(f"Помилка при створенні звіту: {e}") | |
return f"Помилка при створенні звіту: {str(e)}" | |
def create_html_report(self, inactive_days=14, include_visualizations=False, visualization_data=None): | |
""" | |
Створення звіту у форматі HTML. | |
Args: | |
inactive_days (int): Кількість днів для визначення неактивних тікетів | |
include_visualizations (bool): Чи включати візуалізації у звіт | |
visualization_data (dict): Словник з об'єктами Figure для візуалізацій | |
Returns: | |
str: Текст звіту у форматі HTML | |
""" | |
try: | |
# Спочатку створюємо звіт у форматі Markdown | |
md_report = self.create_markdown_report(inactive_days) | |
# Конвертуємо Markdown у HTML | |
html_report = self.convert_markdown_to_html(md_report) | |
# Додаємо візуалізації, якщо потрібно | |
if include_visualizations and visualization_data: | |
html_with_charts = self._add_visualizations_to_html(html_report, visualization_data) | |
return html_with_charts | |
return html_report | |
except Exception as e: | |
logger.error(f"Помилка при створенні HTML звіту: {e}") | |
return f"<h1>Помилка при створенні звіту</h1><p>{str(e)}</p>" | |
def convert_markdown_to_html(self, md_text): | |
""" | |
Конвертація тексту з формату Markdown у HTML. | |
Args: | |
md_text (str): Текст у форматі Markdown | |
Returns: | |
str: Текст у форматі HTML | |
""" | |
try: | |
# Додаємо CSS стилі | |
css = """ | |
<style> | |
body { font-family: Arial, sans-serif; line-height: 1.6; margin: 20px; max-width: 1200px; margin: 0 auto; } | |
h1, h2, h3 { color: #0052CC; } | |
table { border-collapse: collapse; width: 100%; margin-bottom: 20px; } | |
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; } | |
th { background-color: #0052CC; color: white; } | |
tr:hover { background-color: #f5f5f5; } | |
.progress-container { width: 100%; background-color: #f1f1f1; border-radius: 3px; } | |
.progress-bar { height: 20px; border-radius: 3px; } | |
img { max-width: 100%; } | |
</style> | |
""" | |
# Конвертація Markdown в HTML | |
html_content = markdown.markdown(md_text, extensions=['tables', 'fenced_code']) | |
# Складаємо повний HTML документ | |
html = f"""<!DOCTYPE html> | |
<html lang="uk"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Звіт аналізу Jira</title> | |
{css} | |
</head> | |
<body> | |
{html_content} | |
</body> | |
</html> | |
""" | |
return html | |
except Exception as e: | |
logger.error(f"Помилка при конвертації Markdown в HTML: {e}") | |
return f"<h1>Помилка при конвертації звіту</h1><p>{str(e)}</p>" | |
def _add_visualizations_to_html(self, html_content, visualization_data): | |
""" | |
Додавання візуалізацій до HTML звіту. | |
Args: | |
html_content (str): Текст HTML звіту | |
visualization_data (dict): Словник з об'єктами Figure для візуалізацій | |
Returns: | |
str: HTML звіт з візуалізаціями | |
""" | |
try: | |
# Додаємо розділ з візуалізаціями перед закриваючим тегом body | |
charts_html = "<h2>Візуалізації</h2>" | |
# Конвертуємо кожну візуалізацію у base64 та додаємо до HTML | |
for name, fig in visualization_data.items(): | |
if fig: | |
# Зберігаємо фігуру в байтовий потік | |
buf = BytesIO() | |
fig.savefig(buf, format='png', dpi=100) | |
buf.seek(0) | |
# Конвертуємо в base64 | |
img_str = base64.b64encode(buf.read()).decode('utf-8') | |
# Додаємо зображення до HTML | |
title_map = { | |
'status': 'Статуси тікетів', | |
'priority': 'Пріоритети тікетів', | |
'type': 'Типи тікетів', | |
'created_timeline': 'Часова шкала створення тікетів', | |
'inactive': 'Неактивні тікети', | |
'status_timeline': 'Зміна статусів з часом', | |
'lead_time': 'Час виконання тікетів за типами' | |
} | |
title = title_map.get(name, name.replace('_', ' ').title()) | |
charts_html += f""" | |
<div style="text-align: center; margin-bottom: 30px;"> | |
<h3>{title}</h3> | |
<img src="data:image/png;base64,{img_str}" alt="{title}" style="max-width: 100%;"> | |
</div> | |
""" | |
# Вставляємо візуалізації перед закриваючим тегом body | |
html_with_charts = html_content.replace("</body>", f"{charts_html}</body>") | |
return html_with_charts | |
except Exception as e: | |
logger.error(f"Помилка при додаванні візуалізацій до HTML: {e}") | |
return html_content | |
def save_report(self, filepath, format='markdown', include_visualizations=False, visualization_data=None): | |
""" | |
Збереження звіту у файл. | |
Args: | |
filepath (str): Шлях до файлу для збереження | |
format (str): Формат звіту ('markdown', 'html', 'pdf') | |
include_visualizations (bool): Чи включати візуалізації у звіт | |
visualization_data (dict): Словник з об'єктами Figure для візуалізацій | |
Returns: | |
str: Шлях до збереженого файлу або None у випадку помилки | |
""" | |
try: | |
# Створення директорії для файлу, якщо вона не існує | |
directory = os.path.dirname(filepath) | |
if directory and not os.path.exists(directory): | |
os.makedirs(directory) | |
# Вибір формату та створення звіту | |
if format.lower() == 'markdown': | |
report_text = self.create_markdown_report() | |
# Перевірка розширення файлу | |
if not filepath.lower().endswith('.md'): | |
filepath += '.md' | |
# Збереження у файл | |
with open(filepath, 'w', encoding='utf-8') as f: | |
f.write(report_text) | |
elif format.lower() == 'html': | |
html_report = self.create_html_report(include_visualizations=include_visualizations, | |
visualization_data=visualization_data) | |
# Перевірка розширення файлу | |
if not filepath.lower().endswith('.html'): | |
filepath += '.html' | |
# Збереження у файл | |
with open(filepath, 'w', encoding='utf-8') as f: | |
f.write(html_report) | |
elif format.lower() == 'pdf': | |
# Створення спочатку HTML | |
html_report = self.create_html_report(include_visualizations=include_visualizations, | |
visualization_data=visualization_data) | |
# Перевірка розширення файлу | |
if not filepath.lower().endswith('.pdf'): | |
filepath += '.pdf' | |
# Створення тимчасового HTML-файлу | |
temp_html_path = filepath + "_temp.html" | |
with open(temp_html_path, 'w', encoding='utf-8') as f: | |
f.write(html_report) | |
try: | |
# Конвертація HTML в PDF | |
from weasyprint import HTML | |
HTML(filename=temp_html_path).write_pdf(filepath) | |
# Видалення тимчасового HTML-файлу | |
if os.path.exists(temp_html_path): | |
os.remove(temp_html_path) | |
except Exception as e: | |
logger.error(f"Помилка при конвертації в PDF: {e}") | |
return None | |
else: | |
logger.error(f"Непідтримуваний формат звіту: {format}") | |
return None | |
logger.info(f"Звіт успішно збережено у файл: {filepath}") | |
return filepath | |
except Exception as e: | |
logger.error(f"Помилка при збереженні звіту: {e}") | |
return None |