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 |