Files
JIRA_SPRINT_EXPORTER/main.py
David Rice 0fec3e99dc Updates
2026-03-19 14:44:24 +00:00

291 lines
10 KiB
Python

#!/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())