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 @staticmethod 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