Spaces:
Runtime error
Runtime error
import os | |
import json | |
import pandas as pd | |
import logging | |
import requests | |
from datetime import datetime, timedelta | |
from pathlib import Path | |
from jira import JIRA | |
import urllib3 | |
logger = logging.getLogger(__name__) | |
class JiraConnector: | |
""" | |
Клас для взаємодії з API Jira та отримання даних | |
""" | |
def __init__(self, jira_url, jira_username, jira_api_token): | |
""" | |
Ініціалізація з'єднання з Jira. | |
Args: | |
jira_url (str): URL Jira сервера | |
jira_username (str): Ім'я користувача (email) | |
jira_api_token (str): API токен | |
""" | |
self.jira_url = jira_url | |
self.jira_username = jira_username | |
self.jira_api_token = jira_api_token | |
self.jira = self._connect() | |
def _connect(self): | |
""" | |
Підключення до Jira API. | |
Returns: | |
jira.JIRA: Об'єкт для взаємодії з Jira або None у випадку помилки | |
""" | |
try: | |
jira = JIRA( | |
server=self.jira_url, | |
basic_auth=(self.jira_username, self.jira_api_token), | |
options={'timeout': 30} | |
) | |
logger.info("Успішне підключення до Jira") | |
return jira | |
except Exception as e: | |
logger.error(f"Помилка підключення до Jira: {e}") | |
return None | |
def get_project_issues(self, project_key, max_results=500): | |
""" | |
Отримання тікетів проекту. | |
Args: | |
project_key (str): Ключ проекту Jira | |
max_results (int): Максимальна кількість тікетів для отримання | |
Returns: | |
list: Список тікетів або [] у випадку помилки | |
""" | |
try: | |
if self.jira is None: | |
logger.error("Немає з'єднання з Jira") | |
return [] | |
jql = f'project = {project_key} ORDER BY updated DESC' | |
logger.info(f"Виконання JQL запиту: {jql}") | |
issues = self.jira.search_issues( | |
jql, | |
maxResults=max_results, | |
fields="summary,status,issuetype,priority,labels,components,created,updated,assignee,reporter,description,comment" | |
) | |
logger.info(f"Отримано {len(issues)} тікетів для проекту {project_key}") | |
return issues | |
except Exception as e: | |
logger.error(f"Помилка отримання тікетів: {e}") | |
return [] | |
def get_board_issues(self, board_id, project_key, max_results=500): | |
""" | |
Отримання тікетів дошки. | |
Args: | |
board_id (int): ID дошки Jira | |
project_key (str): Ключ проекту для фільтрації | |
max_results (int): Максимальна кількість тікетів для отримання | |
Returns: | |
list: Список тікетів або [] у випадку помилки | |
""" | |
try: | |
if self.jira is None: | |
logger.error("Немає з'єднання з Jira") | |
return [] | |
issues = [] | |
start_at = 0 | |
logger.info(f"Отримання тікетів з дошки ID: {board_id}, проект: {project_key}...") | |
while True: | |
logger.info(f" Отримання тікетів (з {start_at}, максимум 100)...") | |
batch = self.jira.search_issues( | |
f'project = {project_key} ORDER BY updated DESC', | |
startAt=start_at, | |
maxResults=100, | |
fields="summary,status,issuetype,priority,labels,components,created,updated,assignee,reporter,description,comment" | |
) | |
if not batch: | |
break | |
issues.extend(batch) | |
start_at += len(batch) | |
logger.info(f" Отримано {len(batch)} тікетів, загалом {len(issues)}") | |
if len(batch) < 100 or len(issues) >= max_results: | |
break | |
logger.info(f"Загалом отримано {len(issues)} тікетів з дошки {board_id}") | |
return issues | |
except Exception as e: | |
logger.error(f"Помилка отримання тікетів дошки: {e}") | |
logger.error(f"Деталі помилки: {str(e)}") | |
return [] | |
def export_issues_to_csv(self, issues, filepath): | |
""" | |
Експорт тікетів у CSV-файл. | |
Args: | |
issues (list): Список тікетів Jira | |
filepath (str): Шлях для збереження CSV-файлу | |
Returns: | |
pandas.DataFrame: DataFrame з даними або None у випадку помилки | |
""" | |
if not issues: | |
logger.warning("Немає тікетів для експорту") | |
return None | |
try: | |
data = [] | |
for issue in issues: | |
# Визначення даних тікета з коректною обробкою потенційно відсутніх полів | |
issue_data = { | |
'Issue key': issue.key, | |
'Summary': getattr(issue.fields, 'summary', None), | |
'Status': getattr(issue.fields.status, 'name', None) if hasattr(issue.fields, 'status') else None, | |
'Issue Type': getattr(issue.fields.issuetype, 'name', None) if hasattr(issue.fields, 'issuetype') else None, | |
'Priority': getattr(issue.fields.priority, 'name', None) if hasattr(issue.fields, 'priority') else None, | |
'Components': ','.join([c.name for c in issue.fields.components]) if hasattr(issue.fields, 'components') and issue.fields.components else '', | |
'Labels': ','.join(issue.fields.labels) if hasattr(issue.fields, 'labels') and issue.fields.labels else '', | |
'Created': getattr(issue.fields, 'created', None), | |
'Updated': getattr(issue.fields, 'updated', None), | |
'Assignee': getattr(issue.fields.assignee, 'displayName', None) if hasattr(issue.fields, 'assignee') and issue.fields.assignee else None, | |
'Reporter': getattr(issue.fields.reporter, 'displayName', None) if hasattr(issue.fields, 'reporter') and issue.fields.reporter else None, | |
'Description': getattr(issue.fields, 'description', None), | |
'Comments Count': len(issue.fields.comment.comments) if hasattr(issue.fields, 'comment') and hasattr(issue.fields.comment, 'comments') else 0 | |
} | |
# Додаємо коментарі, якщо вони є | |
if hasattr(issue.fields, 'comment') and hasattr(issue.fields.comment, 'comments'): | |
for i, comment in enumerate(issue.fields.comment.comments[:3]): # Беремо перші 3 коментарі | |
issue_data[f'Comment {i+1}'] = comment.body | |
data.append(issue_data) | |
# Створення DataFrame | |
df = pd.DataFrame(data) | |
# Збереження в CSV | |
df.to_csv(filepath, index=False, encoding='utf-8') | |
logger.info(f"Дані експортовано у {filepath}") | |
return df | |
except Exception as e: | |
logger.error(f"Помилка при експорті даних: {e}") | |
return None | |
def get_project_info(self, project_key): | |
""" | |
Отримання інформації про проект. | |
Args: | |
project_key (str): Ключ проекту Jira | |
Returns: | |
dict: Інформація про проект або None у випадку помилки | |
""" | |
try: | |
if self.jira is None: | |
logger.error("Немає з'єднання з Jira") | |
return None | |
project = self.jira.project(project_key) | |
project_info = { | |
'key': project.key, | |
'name': project.name, | |
'lead': project.lead.displayName, | |
'description': project.description, | |
'url': f"{self.jira_url}/projects/{project.key}" | |
} | |
logger.info(f"Отримано інформацію про проект {project_key}") | |
return project_info | |
except Exception as e: | |
logger.error(f"Помилка отримання інформації про проект: {e}") | |
return None | |
def get_boards_list(self, project_key=None): | |
""" | |
Отримання списку дошок. | |
Args: | |
project_key (str): Ключ проекту для фільтрації (необов'язково) | |
Returns: | |
list: Список дошок або [] у випадку помилки | |
""" | |
try: | |
if self.jira is None: | |
logger.error("Немає з'єднання з Jira") | |
return [] | |
# Отримання всіх дошок | |
all_boards = self.jira.boards() | |
# Фільтрація за проектом, якщо вказано | |
if project_key: | |
boards = [] | |
for board in all_boards: | |
# Перевірка, чи дошка належить до вказаного проекту | |
if hasattr(board, 'location') and hasattr(board.location, 'projectKey') and board.location.projectKey == project_key: | |
boards.append(board) | |
# Або якщо назва дошки містить ключ проекту | |
elif project_key in board.name: | |
boards.append(board) | |
else: | |
boards = all_boards | |
# Формування результату | |
result = [] | |
for board in boards: | |
board_info = { | |
'id': board.id, | |
'name': board.name, | |
'type': board.type | |
} | |
if hasattr(board, 'location'): | |
board_info['project_key'] = getattr(board.location, 'projectKey', None) | |
board_info['project_name'] = getattr(board.location, 'projectName', None) | |
result.append(board_info) | |
logger.info(f"Отримано {len(result)} дошок") | |
return result | |
except Exception as e: | |
logger.error(f"Помилка отримання списку дошок: {e}") | |
return [] | |
def get_issue_details(self, issue_key): | |
""" | |
Отримання детальної інформації про тікет. | |
Args: | |
issue_key (str): Ключ тікета | |
Returns: | |
dict: Детальна інформація про тікет або None у випадку помилки | |
""" | |
try: | |
if self.jira is None: | |
logger.error("Немає з'єднання з Jira") | |
return None | |
issue = self.jira.issue(issue_key) | |
# Базова інформація | |
issue_details = { | |
'key': issue.key, | |
'summary': issue.fields.summary, | |
'status': issue.fields.status.name, | |
'issue_type': issue.fields.issuetype.name, | |
'priority': issue.fields.priority.name if hasattr(issue.fields, 'priority') and issue.fields.priority else None, | |
'created': issue.fields.created, | |
'updated': issue.fields.updated, | |
'description': issue.fields.description, | |
'assignee': issue.fields.assignee.displayName if hasattr(issue.fields, 'assignee') and issue.fields.assignee else None, | |
'reporter': issue.fields.reporter.displayName if hasattr(issue.fields, 'reporter') and issue.fields.reporter else None, | |
'url': f"{self.jira_url}/browse/{issue.key}" | |
} | |
# Додаємо коментарі | |
comments = [] | |
if hasattr(issue.fields, 'comment') and hasattr(issue.fields.comment, 'comments'): | |
for comment in issue.fields.comment.comments: | |
comments.append({ | |
'author': comment.author.displayName, | |
'created': comment.created, | |
'body': comment.body | |
}) | |
issue_details['comments'] = comments | |
# Додаємо історію змін | |
changelog = self.jira.issue(issue_key, expand='changelog').changelog | |
history = [] | |
for history_item in changelog.histories: | |
item_info = { | |
'author': history_item.author.displayName, | |
'created': history_item.created, | |
'changes': [] | |
} | |
for item in history_item.items: | |
item_info['changes'].append({ | |
'field': item.field, | |
'from_value': item.fromString, | |
'to_value': item.toString | |
}) | |
history.append(item_info) | |
issue_details['history'] = history | |
logger.info(f"Отримано детальну інформацію про тікет {issue_key}") | |
return issue_details | |
except Exception as e: | |
logger.error(f"Помилка отримання деталей тікета: {e}") | |
return None | |
def test_connection(url, username, api_token): | |
""" | |
Тестування підключення до Jira. | |
Args: | |
url (str): URL Jira сервера | |
username (str): Ім'я користувача (email) | |
api_token (str): API токен | |
Returns: | |
bool: True, якщо підключення успішне, False у іншому випадку | |
""" | |
logger.info(f"Тестування підключення до Jira: {url}") | |
logger.info(f"Користувач: {username}") | |
# Спроба прямого HTTP запиту до сервера | |
try: | |
logger.info("Спроба прямого HTTP запиту до сервера...") | |
response = requests.get( | |
f"{url}/rest/api/2/serverInfo", | |
auth=(username, api_token), | |
timeout=10, | |
verify=True # Змініть на False, якщо у вас самопідписаний сертифікат | |
) | |
logger.info(f"Статус відповіді: {response.status_code}") | |
if response.status_code == 200: | |
logger.info(f"Відповідь: {response.text[:200]}...") | |
return True | |
else: | |
logger.error(f"Помилка: {response.text}") | |
return False | |
except Exception as e: | |
logger.error(f"Помилка HTTP запиту: {type(e).__name__}: {str(e)}") | |
logger.error(f"Деталі винятку: {repr(e)}") | |
return False |