#!/user/bin/env python3 """ JIRA SPRINT EXPORTER - MAIN.PY - ENTRY POINT OF APPLICATION VERSION: 1.0 AUTHOR: D. RICE 18/03/2026 © 2026 ARRIVE """ # Imports import sys from PySide6.QtWidgets import (QApplication, QMainWindow, QTreeWidgetItem, QMessageBox) from PySide6.QtGui import QIcon import requests from requests.auth import HTTPBasicAuth import re import csv import os from ui_mainwindow import Ui_MainWindow # --- CONFIGURATION --- JIRA_DOMAIN = "flowbird.atlassian.net" EMAIL = "david.rice@arrive.com" API_TOKEN = "ATATT3xFfGF0qWIGGGsvwb0oBnekYh88S8jr8XKbwvRkaoFWwq7cPGS2S5gzZkG-o_JXoDuYR5hOSGiL4GIj5XfTfm05mq313yxmkr8DZqVXDdbgwt8HNcxjLPmcWi9cQDy9wJ-Rc17uXIToYZSvZBCHyWiNZQhh5WlTkTwl0yjgGd-x_F-tOn8=F97E27B2" BOARD_ID = 3417 # Replace with your board ID # Main window class class MainWindow(QMainWindow): def __init__(self): # Initialise main window super(MainWindow, self).__init__() # Finish main window initial setup self.ui = Ui_MainWindow() self.ui.setupUi(self) # Window Setup self.setFixedSize(1024, 768) # Add UI Logic self.ui.connButton.setCheckable(True) self.ui.connButton.clicked.connect(self.conn_button_press) # Fetch Data from JIRA sprints = self.fetch_jira_sprints() # Populate Tree Widget self.populate_tree(sprints) def fetch_jira_sprints(self): auth = HTTPBasicAuth(EMAIL, API_TOKEN) headers = {"Accept": "application/json"} sprints_url = f"https://{JIRA_DOMAIN}/rest/agile/1.0/board/{BOARD_ID}/sprint" all_sprints = [] start_at = 0 max_results = 50 # Jira default limit per request print("Fetching Sprints from Jira...") try: while True: params = {"startAt": start_at, "maxResults": max_results} resp = requests.get(sprints_url, headers=headers, auth=auth, params=params) resp.raise_for_status() data = resp.json() all_sprints.extend(data.get("values", [])) if data.get("isLast", True): break start_at += max_results print(f"FOUND {len(all_sprints)} SPRINTS") return all_sprints except Exception as e: print(f"Error fetching Jira data: {e}") return [] def populate_tree(self, sprints): """Sorts sprints numerically and populates the Tree Widget.""" year_parents = {} # Sort the sprints list before processing # This regex looks for 'Sprint' followed by a number and converts it to an integer for sorting def get_sprint_number(s): match = re.search(r"Sprint\s(\d+)", s.get("name", "")) return int(match.group(1)) if match else 0 sorted_sprints = sorted(sprints, key=get_sprint_number) # Process the sorted list pattern = r"(.*)\s(W\d{2})_(W\d{2})_(\d{4})" for sprint in sorted_sprints: s_name = sprint.get("name", "Unknown Sprint") s_id = sprint.get("id", "N/A") match = re.search(pattern, s_name) if match: prefix, w1, w2, year = match.groups() if year not in year_parents: parent = QTreeWidgetItem(self.ui.tree) parent.setText(0, year) year_parents[year] = parent cleaned_name = f"{prefix.strip()} {w1}-{w2} (ID: {s_id})" child = QTreeWidgetItem(year_parents[year]) child.setText(0, cleaned_name) else: # Fallback for "HW Sprint 1" which might be missing the Wxx_Wxx part fallback_key = "Other" if fallback_key not in year_parents: parent = QTreeWidgetItem(self.ui.tree) parent.setText(0, fallback_key) year_parents[fallback_key] = parent child = QTreeWidgetItem(year_parents[fallback_key]) child.setText(0, f"{s_name} (ID: {s_id})") def conn_button_press(self): # Get the currently selected item in the tree selected_items = self.ui.tree.selectedItems() msg = QMessageBox() msg.setIcon(QMessageBox.Icon.Warning) # Get the absolute path of the directory where the program resides current_dir = os.path.dirname(os.path.abspath(__file__)) ico_path = os.path.join(current_dir, "arriveico.png") msg.setWindowIcon(QIcon(ico_path)) msg_style = """ QMessageBox { background-image: url(""); background-color: #FF80D4; border: 1px solid #FF33BB; border-radius: 10px; font-weight: bold; font: 10pt 'Optimism Sans'; } /* The OK button font and style */ QPushButton { background-color: #FF80D4; font-family: "Segoe UI", Arial; /* Set your font here */ font: 10pt 'Optimism Sans'; } """ msg.setStyleSheet(msg_style) if not selected_items: msg.setWindowTitle("SELECTION REQUIRED") msg.setText("PLEASE SELECT A SPRINT FROM THE TREE FIRST") msg.exec() return item = selected_items[0] # Check if it's a child (sprint) and not a parent (year) if item.parent() is None: msg.setWindowTitle("INVALID SELECTION") msg.setText("PLEASE SELECT A SPECIFIC SPRINT, NOT A YEAR CATEGORY") msg.exec() return # Extract ID from string "HW Sprint 2 W04-W05 (ID: 18932)" text = item.text(0) match = re.search(r"\(ID:\s(\d+)\)", text) if not match: return sprint_id = match.group(1) self.export_sprint_to_csv(sprint_id) def export_sprint_to_csv(self, sprint_id): # Fetch issues for this specific sprint issues_url = f"https://{JIRA_DOMAIN}/rest/agile/1.0/sprint/{sprint_id}/issue" auth = HTTPBasicAuth(EMAIL, API_TOKEN) msg = QMessageBox() msg.setIcon(QMessageBox.Icon.Warning) # Get the absolute path of the directory where the program resides current_dir = os.path.dirname(os.path.abspath(__file__)) ico_path = os.path.join(current_dir, "arriveico.png") msg.setWindowIcon(QIcon(ico_path)) msg_style = """ QMessageBox { background-image: url(""); background-color: #FF80D4; border: 1px solid #FF33BB; border-radius: 10px; font-weight: bold; font: 10pt 'Optimism Sans'; } /* The OK button font and style */ QPushButton { background-color: #FF80D4; font-family: "Segoe UI", Arial; /* Set your font here */ font: 10pt 'Optimism Sans'; font-weight: bold; } """ msg.setStyleSheet(msg_style) try: resp = requests.get(issues_url, auth=auth) resp.raise_for_status() issues = resp.json().get("issues", []) if not issues: msg.setWindowTitle("NO DATA") msg.setText("THIS SPRINT CONTAINS NO ISSUES") msg.exec() #QMessageBox.information(self, "No Data", "This sprint contains no issues.") return # Write to CSV # Define your target folder # Get the absolute path of the directory where the program resides current_dir = os.path.dirname(os.path.abspath(__file__)) target_folder = os.path.join(current_dir, "EXPORT") #target_folder = r"C:\Users\david.rice\Documents\Python\ARRIVE\EXPORTS" # Create the folder if it doesn't exist yet if not os.path.exists(target_folder): os.makedirs(target_folder) filename = f"SPRINT_{sprint_id}_EXPORT.csv" # Join them together into one path full_path = os.path.join(target_folder, filename) with open(full_path, mode='w', newline='', encoding='utf-8') as f: writer = csv.writer(f) # Header row writer.writerow(["Key", "Summary", "Labels", "Original Estimate (Hrs)", "Status", "Priority", "Assignee"]) for issue in issues: fields = issue.get("fields", {}) # Get Labels and join them into one string labels_list = fields.get("labels", []) label_text = ", ".join(labels_list) if labels_list else "NONE" # Get Original Estimate (Seconds -> Hours) # Jira returns None if no estimate is set, so we default to 0 seconds = fields.get("timeoriginalestimate") estimate_hrs = (seconds / 3600) if seconds else 0 writer.writerow([ issue.get("key"), fields.get("summary"), label_text, f"{estimate_hrs:.2f}", fields.get("status", {}).get("name"), fields.get("priority", {}).get("name"), fields.get("assignee", {}).get("displayName") if fields.get("assignee") else "UNASSIGNED" ]) msg.setIcon(QMessageBox.Icon.Information) msg.setWindowTitle("SUCCESS") msg.setText(f"EXPORTED {len(issues)} ISSUES TO {filename}") msg.exec() except Exception as e: msg.setIcon(QMessageBox.Icon.Critical) msg.setWindowTitle("ERROR") msg.setText(f"FAILED TO EXPORT: {str(e)}") msg.exec() # Run main if __name__ == '__main__': # Launch main window app = QApplication(sys.argv) app.setStyle('Fusion') window = MainWindow() window.show() sys.exit(app.exec())