Spaces:
Sleeping
Sleeping
Commit
Β·
295a9df
1
Parent(s):
69a44c9
added auto loader that doesnt work
Browse files- app.py +62 -16
- environment_loader.py +137 -0
- jira_integration.py +73 -99
- multiple.py +164 -126
- multiple_env_loader.py +120 -0
- pre.py +42 -19
app.py
CHANGED
@@ -4,9 +4,12 @@ import matplotlib.pyplot as plt
|
|
4 |
import numpy as np
|
5 |
from second import double_main
|
6 |
from multiple import multiple_main
|
|
|
|
|
7 |
from weekly import generate_weekly_report
|
8 |
from pre import preprocess_uploaded_file, add_app_description
|
9 |
-
from multi_env_compare import multi_env_compare_main
|
|
|
10 |
|
11 |
|
12 |
|
@@ -143,33 +146,73 @@ def single_main(uploaded_file):
|
|
143 |
def main():
|
144 |
add_app_description()
|
145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
146 |
# Initialize session state for mode if it doesn't exist
|
147 |
if "mode" not in st.session_state:
|
148 |
st.session_state["mode"] = "multi"
|
149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
150 |
# Initialize session state for the selectbox widget
|
151 |
if "selected_mode" not in st.session_state:
|
152 |
st.session_state["selected_mode"] = "Multi"
|
153 |
-
|
154 |
-
# Use the session state for the default value of the selectbox
|
155 |
-
selected_mode = st.sidebar.selectbox(
|
156 |
-
"Select Mode",
|
157 |
-
["Multi", "Compare", "Weekly", "Multi-Env Compare"],
|
158 |
-
index=["Multi", "Compare", "Weekly", "Multi-Env Compare"].index(st.session_state["selected_mode"])
|
159 |
-
)
|
160 |
|
161 |
-
#
|
162 |
-
st.session_state["selected_mode"] = selected_mode
|
163 |
-
st.session_state["mode"] = selected_mode.lower()
|
164 |
-
|
165 |
-
mode_display = f'## Current mode: {st.session_state["mode"].title()} mode'
|
166 |
-
st.sidebar.markdown(mode_display)
|
167 |
-
|
168 |
if st.session_state["mode"] == "multi":
|
169 |
multiple_main()
|
170 |
elif st.session_state["mode"] == "compare":
|
171 |
st.sidebar.markdown("### Upload Files for Comparison")
|
172 |
-
|
|
|
|
|
173 |
|
174 |
if upload_option == "Single uploader":
|
175 |
uploaded_files = st.sidebar.file_uploader("Upload CSV or XLSX files for comparison", type=["csv", "xlsx"], accept_multiple_files=True)
|
@@ -201,6 +244,9 @@ def main():
|
|
201 |
generate_weekly_report(uploaded_files)
|
202 |
elif st.session_state["mode"] == "multi-env compare":
|
203 |
multi_env_compare_main()
|
|
|
|
|
|
|
204 |
|
205 |
if __name__ == "__main__":
|
206 |
main()
|
|
|
4 |
import numpy as np
|
5 |
from second import double_main
|
6 |
from multiple import multiple_main
|
7 |
+
from multiple import display_story_points_stats
|
8 |
+
from jira_integration import render_jira_login, JIRA_SERVER
|
9 |
from weekly import generate_weekly_report
|
10 |
from pre import preprocess_uploaded_file, add_app_description
|
11 |
+
from multi_env_compare import multi_env_compare_main
|
12 |
+
import multiple_env_loader
|
13 |
|
14 |
|
15 |
|
|
|
146 |
def main():
|
147 |
add_app_description()
|
148 |
|
149 |
+
# --- Centralized Sidebar Initialization ---
|
150 |
+
# Initialize session state for Jira and sprint data if they don't exist
|
151 |
+
if 'jira_server' not in st.session_state:
|
152 |
+
st.session_state.jira_server = JIRA_SERVER
|
153 |
+
if 'is_authenticated' not in st.session_state:
|
154 |
+
st.session_state.is_authenticated = False # Start as not authenticated
|
155 |
+
if 'jira_client' not in st.session_state:
|
156 |
+
st.session_state.jira_client = None
|
157 |
+
if 'sprint_data_initialized' not in st.session_state:
|
158 |
+
st.session_state.sprint_data_initialized = False
|
159 |
+
if 'force_sprint_refresh' not in st.session_state:
|
160 |
+
st.session_state.force_sprint_refresh = False
|
161 |
+
if 'sprint_data_cache' not in st.session_state:
|
162 |
+
st.session_state.sprint_data_cache = None
|
163 |
+
if 'last_sprint_fetch' not in st.session_state:
|
164 |
+
st.session_state.last_sprint_fetch = None
|
165 |
+
|
166 |
# Initialize session state for mode if it doesn't exist
|
167 |
if "mode" not in st.session_state:
|
168 |
st.session_state["mode"] = "multi"
|
169 |
|
170 |
+
# --- Sidebar Rendering ---
|
171 |
+
with st.sidebar:
|
172 |
+
# Mode Selection (kept in sidebar)
|
173 |
+
selected_mode = st.selectbox(
|
174 |
+
"Select Mode",
|
175 |
+
["Multi", "Compare", "Weekly", "Multi-Env Compare", "Auto Environment Loader"],
|
176 |
+
index=["Multi", "Compare", "Weekly", "Multi-Env Compare", "Auto Environment Loader"].index(st.session_state.get("selected_mode", "Multi"))
|
177 |
+
)
|
178 |
+
# Update the session state with the new selection
|
179 |
+
st.session_state["selected_mode"] = selected_mode
|
180 |
+
st.session_state["mode"] = selected_mode.lower()
|
181 |
+
mode_display = f'## Current mode: {st.session_state["mode"].title()} mode'
|
182 |
+
st.markdown(mode_display)
|
183 |
+
|
184 |
+
st.markdown("---") # Separator
|
185 |
+
|
186 |
+
# Jira Login Expander (always shown)
|
187 |
+
with st.expander("Jira Integration (Optional)", expanded=True):
|
188 |
+
# Render login - function handles checking if already authenticated
|
189 |
+
# It updates st.session_state.is_authenticated and st.session_state.jira_client
|
190 |
+
st.session_state.is_authenticated = render_jira_login()
|
191 |
+
|
192 |
+
# Sprint Progress Expander (shown only if authenticated)
|
193 |
+
if st.session_state.is_authenticated and st.session_state.jira_client:
|
194 |
+
st.markdown("---") # Separator inside the main expander
|
195 |
+
with st.expander("Sprint Progress", expanded=True):
|
196 |
+
# Refresh button
|
197 |
+
if st.button("π Refresh Sprint Data", key="refresh_sprint_sidebar_app"):
|
198 |
+
st.session_state.force_sprint_refresh = True
|
199 |
+
# Always call display (it handles caching), passing manual refresh flag
|
200 |
+
display_story_points_stats(force_refresh=st.session_state.force_sprint_refresh)
|
201 |
+
# Reset manual refresh flag after use
|
202 |
+
st.session_state.force_sprint_refresh = False
|
203 |
+
|
204 |
# Initialize session state for the selectbox widget
|
205 |
if "selected_mode" not in st.session_state:
|
206 |
st.session_state["selected_mode"] = "Multi"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
207 |
|
208 |
+
# --- Main Page Content based on Mode ---
|
|
|
|
|
|
|
|
|
|
|
|
|
209 |
if st.session_state["mode"] == "multi":
|
210 |
multiple_main()
|
211 |
elif st.session_state["mode"] == "compare":
|
212 |
st.sidebar.markdown("### Upload Files for Comparison")
|
213 |
+
# Move file uploaders to main page area if needed, or keep in sidebar below Jira?
|
214 |
+
# For now, keeping in sidebar as it was.
|
215 |
+
upload_option = st.sidebar.radio("Upload method", ["Single uploader", "Two separate uploaders"], key="compare_upload_method")
|
216 |
|
217 |
if upload_option == "Single uploader":
|
218 |
uploaded_files = st.sidebar.file_uploader("Upload CSV or XLSX files for comparison", type=["csv", "xlsx"], accept_multiple_files=True)
|
|
|
244 |
generate_weekly_report(uploaded_files)
|
245 |
elif st.session_state["mode"] == "multi-env compare":
|
246 |
multi_env_compare_main()
|
247 |
+
elif st.session_state["mode"] == "auto environment loader":
|
248 |
+
# Launch the auto environment loader workflow
|
249 |
+
multiple_env_loader.main()
|
250 |
|
251 |
if __name__ == "__main__":
|
252 |
main()
|
environment_loader.py
ADDED
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# environment_loader.py
|
2 |
+
'''Utility module to load .xlsx files from environment-specific folders and optional Huggingface repositories.'''
|
3 |
+
|
4 |
+
import os
|
5 |
+
from pathlib import Path
|
6 |
+
import pandas as pd
|
7 |
+
from datetime import datetime, timedelta
|
8 |
+
|
9 |
+
# Optional Huggingface support
|
10 |
+
try:
|
11 |
+
from huggingface_hub import hf_hub_download
|
12 |
+
except ImportError:
|
13 |
+
hf_hub_download = None
|
14 |
+
|
15 |
+
|
16 |
+
def get_latest_file_in_directory(directory_path: str, pattern: str = '*.xlsx') -> Path:
|
17 |
+
'''Return the Path to the latest modified file matching pattern in directory_path.'''
|
18 |
+
dir_path = Path(directory_path)
|
19 |
+
files = list(dir_path.glob(pattern))
|
20 |
+
if not files:
|
21 |
+
return None
|
22 |
+
return max(files, key=lambda f: f.stat().st_mtime)
|
23 |
+
|
24 |
+
|
25 |
+
def get_file_by_date(directory_path: str, target_date: datetime.date, pattern: str = '*.xlsx') -> Path:
|
26 |
+
'''Return Path to the file whose name contains target_date or whose modification date matches target_date.'''
|
27 |
+
date_str = target_date.strftime('%Y-%m-%d')
|
28 |
+
dir_path = Path(directory_path)
|
29 |
+
# First try matching date string in filename
|
30 |
+
candidates = [f for f in dir_path.glob(pattern) if date_str in f.name]
|
31 |
+
if candidates:
|
32 |
+
return max(candidates, key=lambda f: f.stat().st_mtime)
|
33 |
+
# Fallback to checking file modification date
|
34 |
+
files = [f for f in dir_path.glob(pattern) if datetime.fromtimestamp(f.stat().st_mtime).date() == target_date]
|
35 |
+
if files:
|
36 |
+
return max(files, key=lambda f: f.stat().st_mtime)
|
37 |
+
return None
|
38 |
+
|
39 |
+
|
40 |
+
def load_latest_xlsx_for_env(env_code: str,
|
41 |
+
base_path: str = 'Q:/Selenium_Reports',
|
42 |
+
use_date: bool = False,
|
43 |
+
target_date: datetime.date = None) -> pd.DataFrame:
|
44 |
+
'''Load the latest or date-specific .xlsx file for the given environment code.'''
|
45 |
+
folder = Path(base_path) / env_code / 'XLSX'
|
46 |
+
if not folder.exists():
|
47 |
+
raise FileNotFoundError(f"Environment folder not found: {folder}")
|
48 |
+
if use_date:
|
49 |
+
if target_date is None:
|
50 |
+
raise ValueError('target_date must be provided when use_date is True')
|
51 |
+
file_path = get_file_by_date(folder, target_date)
|
52 |
+
else:
|
53 |
+
file_path = get_latest_file_in_directory(folder)
|
54 |
+
if file_path is None:
|
55 |
+
raise FileNotFoundError(f'No .xlsx files found for environment {env_code} in {folder}')
|
56 |
+
return pd.read_excel(file_path)
|
57 |
+
|
58 |
+
|
59 |
+
def load_environments(env_codes: list,
|
60 |
+
base_path: str = 'Q:/Selenium_Reports',
|
61 |
+
by_date: bool = False,
|
62 |
+
days_ago: int = 1) -> dict:
|
63 |
+
'''Load DataFrame for each environment code; by_date chooses file from days_ago days before.'''
|
64 |
+
data = {}
|
65 |
+
target_date = datetime.now().date() - timedelta(days=days_ago)
|
66 |
+
for env in env_codes:
|
67 |
+
df = load_latest_xlsx_for_env(
|
68 |
+
env_code=env,
|
69 |
+
base_path=base_path,
|
70 |
+
use_date=by_date,
|
71 |
+
target_date=target_date
|
72 |
+
)
|
73 |
+
data[env] = df
|
74 |
+
return data
|
75 |
+
|
76 |
+
|
77 |
+
def load_from_huggingface(repo_id: str,
|
78 |
+
filenames: list,
|
79 |
+
revision: str = 'main') -> dict:
|
80 |
+
'''Download files from a Huggingface repo and load as DataFrames.'''
|
81 |
+
if hf_hub_download is None:
|
82 |
+
raise ImportError('huggingface_hub is not installed. Please pip install huggingface_hub')
|
83 |
+
data = {}
|
84 |
+
for fname in filenames:
|
85 |
+
local_path = hf_hub_download(repo_id=repo_id, filename=fname, revision=revision)
|
86 |
+
data[fname] = pd.read_excel(local_path)
|
87 |
+
return data
|
88 |
+
|
89 |
+
|
90 |
+
def get_latest_xlsx_path_for_env(env_code: str,
|
91 |
+
base_path: str = 'Q:/Selenium_Reports',
|
92 |
+
use_date: bool = False,
|
93 |
+
target_date: datetime.date = None) -> Path:
|
94 |
+
'''Return the Path to the desired .xlsx file for the given environment code without loading.'''
|
95 |
+
folder = Path(base_path) / env_code / 'XLSX'
|
96 |
+
if not folder.exists():
|
97 |
+
raise FileNotFoundError(f"Environment folder not found: {folder}")
|
98 |
+
if use_date:
|
99 |
+
if target_date is None:
|
100 |
+
raise ValueError('target_date must be provided when use_date is True')
|
101 |
+
file_path = get_file_by_date(folder, target_date)
|
102 |
+
else:
|
103 |
+
file_path = get_latest_file_in_directory(folder)
|
104 |
+
if file_path is None:
|
105 |
+
raise FileNotFoundError(f'No .xlsx files found for environment {env_code} in {folder}')
|
106 |
+
return file_path
|
107 |
+
|
108 |
+
|
109 |
+
def get_environments_paths(env_codes: list,
|
110 |
+
base_path: str = 'Q:/Selenium_Reports',
|
111 |
+
by_date: bool = False,
|
112 |
+
days_ago: int = 1) -> dict:
|
113 |
+
'''Return file Paths for each environment code; by_date chooses file from days_ago days before.'''
|
114 |
+
data = {}
|
115 |
+
target_date = datetime.now().date() - timedelta(days=days_ago)
|
116 |
+
for env in env_codes:
|
117 |
+
path = get_latest_xlsx_path_for_env(
|
118 |
+
env_code=env,
|
119 |
+
base_path=base_path,
|
120 |
+
use_date=by_date,
|
121 |
+
target_date=target_date
|
122 |
+
)
|
123 |
+
data[env] = path
|
124 |
+
return data
|
125 |
+
|
126 |
+
|
127 |
+
def get_huggingface_paths(repo_id: str,
|
128 |
+
filenames: list,
|
129 |
+
revision: str = 'main') -> dict:
|
130 |
+
'''Download files from a Huggingface repo and return local Paths without loading.'''
|
131 |
+
if hf_hub_download is None:
|
132 |
+
raise ImportError('huggingface_hub is not installed. Please pip install huggingface_hub')
|
133 |
+
data = {}
|
134 |
+
for fname in filenames:
|
135 |
+
local_path = hf_hub_download(repo_id=repo_id, filename=fname, revision=revision)
|
136 |
+
data[fname] = Path(local_path)
|
137 |
+
return data
|
jira_integration.py
CHANGED
@@ -10,7 +10,6 @@ from datetime import datetime, timedelta
|
|
10 |
import pandas as pd
|
11 |
import requests
|
12 |
import json
|
13 |
-
from groq import Groq
|
14 |
from difflib import SequenceMatcher
|
15 |
import time
|
16 |
|
@@ -48,13 +47,10 @@ load_dotenv()
|
|
48 |
|
49 |
# Get API keys and configuration with default values for development
|
50 |
JIRA_SERVER = os.getenv("JIRA_SERVER")
|
51 |
-
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
52 |
|
53 |
# Validate required environment variables
|
54 |
if not JIRA_SERVER:
|
55 |
st.error("JIRA_SERVER not found in environment variables. Please check your .env file.")
|
56 |
-
if not GROQ_API_KEY:
|
57 |
-
st.error("GROQ_API_KEY not found in environment variables. Please check your .env file.")
|
58 |
|
59 |
def init_jira_session():
|
60 |
"""Initialize Jira session state variables"""
|
@@ -1484,102 +1480,70 @@ def display_story_points_stats(force_refresh=False):
|
|
1484 |
"""Display story points statistics from current sprint"""
|
1485 |
if not st.session_state.jira_client:
|
1486 |
return
|
1487 |
-
|
1488 |
-
# Initialize
|
1489 |
if 'sprint_data' not in st.session_state:
|
1490 |
st.session_state.sprint_data = None
|
1491 |
-
|
1492 |
-
# Initialize refresh timestamp if not exists
|
1493 |
if 'last_sprint_refresh' not in st.session_state:
|
1494 |
st.session_state.last_sprint_refresh = None
|
1495 |
-
|
1496 |
-
|
1497 |
-
|
1498 |
-
|
1499 |
-
|
1500 |
-
|
1501 |
-
|
1502 |
-
|
1503 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1504 |
)
|
1505 |
-
|
1506 |
-
if refresh_needed:
|
1507 |
-
with st.spinner("Fetching sprint data..."):
|
1508 |
-
# Get regression board
|
1509 |
-
board = get_regression_board("RS")
|
1510 |
-
if not board:
|
1511 |
-
return
|
1512 |
-
|
1513 |
-
# Get current sprint
|
1514 |
-
sprint = get_current_sprint(board['id'])
|
1515 |
-
if not sprint:
|
1516 |
-
return
|
1517 |
-
|
1518 |
-
# Get sprint issues
|
1519 |
-
issues = get_sprint_issues(board['id'], sprint.id, board['estimation_field'])
|
1520 |
-
if not issues:
|
1521 |
-
return
|
1522 |
-
|
1523 |
-
# Calculate points
|
1524 |
-
issues_data, total_points, completed_points, in_progress_points = calculate_points(issues, board['estimation_field'])
|
1525 |
-
|
1526 |
-
# Store in session state
|
1527 |
-
st.session_state.sprint_data = {
|
1528 |
-
'sprint_name': sprint.name,
|
1529 |
-
'total_points': total_points,
|
1530 |
-
'completed_points': completed_points,
|
1531 |
-
'in_progress_points': in_progress_points,
|
1532 |
-
'timestamp': current_time
|
1533 |
-
}
|
1534 |
-
st.session_state.last_sprint_refresh = current_time
|
1535 |
-
|
1536 |
-
# Display data from session state
|
1537 |
-
if st.session_state.sprint_data:
|
1538 |
-
sprint_data = st.session_state.sprint_data
|
1539 |
-
|
1540 |
-
# Create compact metrics display using custom HTML/CSS
|
1541 |
-
st.markdown(f"""
|
1542 |
-
<div style='background-color: #1E1E1E; padding: 10px; border-radius: 5px; margin-bottom: 10px;'>
|
1543 |
-
<div style='font-size: 0.8em; color: #E0E0E0; margin-bottom: 8px;'>Current Sprint: {sprint_data['sprint_name']}</div>
|
1544 |
-
<div style='display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; font-size: 0.9em;'>
|
1545 |
-
<div style='text-align: center;'>
|
1546 |
-
<div style='color: #E0E0E0;'>Total</div>
|
1547 |
-
<div style='font-size: 1.2em; font-weight: bold;'>{sprint_data['total_points']:.1f}</div>
|
1548 |
-
</div>
|
1549 |
-
<div style='text-align: center;'>
|
1550 |
-
<div style='color: #E0E0E0;'>Done</div>
|
1551 |
-
<div style='font-size: 1.2em; font-weight: bold;'>{sprint_data['completed_points']:.1f}</div>
|
1552 |
-
</div>
|
1553 |
-
<div style='text-align: center;'>
|
1554 |
-
<div style='color: #E0E0E0;'>In Progress</div>
|
1555 |
-
<div style='font-size: 1.2em; font-weight: bold;'>{sprint_data['in_progress_points']:.1f}</div>
|
1556 |
-
</div>
|
1557 |
-
<div style='text-align: center;'>
|
1558 |
-
<div style='color: #E0E0E0;'>Complete</div>
|
1559 |
-
<div style='font-size: 1.2em; font-weight: bold;'>{(sprint_data['completed_points'] / sprint_data['total_points'] * 100) if sprint_data['total_points'] > 0 else 0:.1f}%</div>
|
1560 |
-
</div>
|
1561 |
-
</div>
|
1562 |
-
</div>
|
1563 |
-
""", unsafe_allow_html=True)
|
1564 |
-
|
1565 |
-
# Show progress bar
|
1566 |
-
progress = sprint_data['completed_points'] / sprint_data['total_points'] if sprint_data['total_points'] > 0 else 0
|
1567 |
-
st.progress(progress)
|
1568 |
-
|
1569 |
-
# Add refresh button with key based on timestamp to prevent rerendering
|
1570 |
-
refresh_key = f"refresh_stats_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
1571 |
-
if st.button("π Refresh", key=refresh_key, use_container_width=True):
|
1572 |
-
# Use a session state flag to trigger refresh on next rerun
|
1573 |
-
st.session_state.force_sprint_refresh = True
|
1574 |
-
st.rerun()
|
1575 |
-
|
1576 |
-
except Exception as e:
|
1577 |
-
st.error(f"Error updating story points: {str(e)}")
|
1578 |
-
|
1579 |
-
# Check if we need to force refresh (from button click)
|
1580 |
-
if 'force_sprint_refresh' in st.session_state and st.session_state.force_sprint_refresh:
|
1581 |
-
st.session_state.force_sprint_refresh = False
|
1582 |
-
return display_story_points_stats(force_refresh=True)
|
1583 |
|
1584 |
def main():
|
1585 |
st.title("Jira Integration Test")
|
@@ -1588,10 +1552,20 @@ def main():
|
|
1588 |
if st.button("Load Test Data"):
|
1589 |
st.session_state.filtered_scenarios_df = create_test_data()
|
1590 |
st.success("Test data loaded!")
|
1591 |
-
|
1592 |
-
|
1593 |
-
|
1594 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1595 |
# Fixed project and board selection
|
1596 |
project_key = "RS"
|
1597 |
board_type = "scrum"
|
|
|
10 |
import pandas as pd
|
11 |
import requests
|
12 |
import json
|
|
|
13 |
from difflib import SequenceMatcher
|
14 |
import time
|
15 |
|
|
|
47 |
|
48 |
# Get API keys and configuration with default values for development
|
49 |
JIRA_SERVER = os.getenv("JIRA_SERVER")
|
|
|
50 |
|
51 |
# Validate required environment variables
|
52 |
if not JIRA_SERVER:
|
53 |
st.error("JIRA_SERVER not found in environment variables. Please check your .env file.")
|
|
|
|
|
54 |
|
55 |
def init_jira_session():
|
56 |
"""Initialize Jira session state variables"""
|
|
|
1480 |
"""Display story points statistics from current sprint"""
|
1481 |
if not st.session_state.jira_client:
|
1482 |
return
|
1483 |
+
|
1484 |
+
# Initialize sprint data cache
|
1485 |
if 'sprint_data' not in st.session_state:
|
1486 |
st.session_state.sprint_data = None
|
|
|
|
|
1487 |
if 'last_sprint_refresh' not in st.session_state:
|
1488 |
st.session_state.last_sprint_refresh = None
|
1489 |
+
|
1490 |
+
current_time = datetime.now()
|
1491 |
+
cache_expiry = 300 # 5 minutes
|
1492 |
+
# Determine if a data refresh is needed
|
1493 |
+
refresh_needed = (
|
1494 |
+
force_refresh
|
1495 |
+
or st.session_state.sprint_data is None
|
1496 |
+
or (st.session_state.last_sprint_refresh
|
1497 |
+
and (current_time - st.session_state.last_sprint_refresh).total_seconds() > cache_expiry)
|
1498 |
+
)
|
1499 |
+
|
1500 |
+
if refresh_needed:
|
1501 |
+
# Only show spinner when fetching new data
|
1502 |
+
with st.spinner("Fetching sprint data..."):
|
1503 |
+
board = get_regression_board("RS")
|
1504 |
+
if not board:
|
1505 |
+
return
|
1506 |
+
sprint = get_current_sprint(board['id'])
|
1507 |
+
if not sprint:
|
1508 |
+
return
|
1509 |
+
issues = get_sprint_issues(board['id'], sprint.id, board['estimation_field'])
|
1510 |
+
if not issues:
|
1511 |
+
return
|
1512 |
+
# Calculate story points
|
1513 |
+
_, total_points, completed_points, in_progress_points = calculate_points(
|
1514 |
+
issues, board['estimation_field']
|
1515 |
+
)
|
1516 |
+
# Cache results
|
1517 |
+
st.session_state.sprint_data = {
|
1518 |
+
'sprint_name': sprint.name,
|
1519 |
+
'total_points': total_points,
|
1520 |
+
'completed_points': completed_points,
|
1521 |
+
'in_progress_points': in_progress_points
|
1522 |
+
}
|
1523 |
+
st.session_state.last_sprint_refresh = current_time
|
1524 |
+
|
1525 |
+
# Display cached sprint data
|
1526 |
+
if st.session_state.sprint_data:
|
1527 |
+
sprint_data = st.session_state.sprint_data
|
1528 |
+
cols = st.columns(4)
|
1529 |
+
with cols[0]:
|
1530 |
+
st.metric("Total", f"{sprint_data['total_points']:.1f}")
|
1531 |
+
with cols[1]:
|
1532 |
+
st.metric("Done", f"{sprint_data['completed_points']:.1f}")
|
1533 |
+
with cols[2]:
|
1534 |
+
st.metric("In Progress", f"{sprint_data['in_progress_points']:.1f}")
|
1535 |
+
with cols[3]:
|
1536 |
+
completion_rate = (
|
1537 |
+
sprint_data['completed_points'] / sprint_data['total_points'] * 100
|
1538 |
+
if sprint_data['total_points'] > 0 else 0
|
1539 |
+
)
|
1540 |
+
st.metric("Complete", f"{completion_rate:.1f}%")
|
1541 |
+
# Show progress bar
|
1542 |
+
progress = (
|
1543 |
+
sprint_data['completed_points'] / sprint_data['total_points']
|
1544 |
+
if sprint_data['total_points'] > 0 else 0
|
1545 |
)
|
1546 |
+
st.progress(progress)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1547 |
|
1548 |
def main():
|
1549 |
st.title("Jira Integration Test")
|
|
|
1552 |
if st.button("Load Test Data"):
|
1553 |
st.session_state.filtered_scenarios_df = create_test_data()
|
1554 |
st.success("Test data loaded!")
|
1555 |
+
|
1556 |
+
# Sidebar: Jira login control
|
1557 |
+
with st.sidebar:
|
1558 |
+
if 'is_authenticated' not in st.session_state:
|
1559 |
+
st.session_state.is_authenticated = False
|
1560 |
+
if st.session_state.is_authenticated and 'jira_client' in st.session_state and st.session_state.jira_client:
|
1561 |
+
st.success("Connected to Jira")
|
1562 |
+
else:
|
1563 |
+
if st.button("Connect to Jira"):
|
1564 |
+
is_authenticated = render_jira_login()
|
1565 |
+
st.session_state.is_authenticated = is_authenticated
|
1566 |
+
st.experimental_rerun()
|
1567 |
+
|
1568 |
+
if st.session_state.get('is_authenticated', False) and st.session_state.get('projects'):
|
1569 |
# Fixed project and board selection
|
1570 |
project_key = "RS"
|
1571 |
board_type = "scrum"
|
multiple.py
CHANGED
@@ -25,6 +25,29 @@ import os
|
|
25 |
from dotenv import load_dotenv
|
26 |
import json
|
27 |
import logging
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
load_dotenv()
|
29 |
JIRA_SERVER = os.getenv("JIRA_SERVER")
|
30 |
# Initialize session state variables
|
@@ -237,21 +260,18 @@ def perform_analysis(uploaded_dataframes):
|
|
237 |
grouped_filtered_scenarios.index = range(1, len(grouped_filtered_scenarios) + 1)
|
238 |
st.dataframe(grouped_filtered_scenarios)
|
239 |
|
240 |
-
#
|
241 |
-
|
242 |
-
|
243 |
-
# 3. Exactly one functional area is selected (not "All")
|
244 |
-
if ('jira_client' in st.session_state and
|
245 |
st.session_state.jira_client and
|
246 |
selected_status == 'Failed' and
|
247 |
len(selected_functional_areas) == 1 and
|
248 |
-
"All" not in selected_functional_areas
|
249 |
-
|
250 |
-
|
|
|
251 |
if st.session_state.show_success and st.session_state.last_task_key:
|
252 |
st.success("β
Task created successfully!")
|
253 |
-
|
254 |
-
# Display task link in a more prominent way
|
255 |
st.markdown(
|
256 |
f"""
|
257 |
<div style='padding: 10px; border-radius: 5px; border: 1px solid #90EE90; margin: 10px 0;'>
|
@@ -266,46 +286,42 @@ def perform_analysis(uploaded_dataframes):
|
|
266 |
""",
|
267 |
unsafe_allow_html=True
|
268 |
)
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
st.session_state.task_content = None
|
276 |
-
st.session_state.last_task_key = None
|
277 |
-
st.session_state.last_task_url = None
|
278 |
-
st.session_state.show_success = False
|
279 |
-
st.rerun()
|
280 |
else:
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
|
|
309 |
|
310 |
# Check if selected_status is 'Failed' and show bar graph
|
311 |
if selected_status != 'Passed':
|
@@ -329,8 +345,19 @@ def perform_analysis(uploaded_dataframes):
|
|
329 |
# Display individual numbers on y-axis
|
330 |
for bar in bars:
|
331 |
height = bar.get_height()
|
332 |
-
|
333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
334 |
|
335 |
plt.tight_layout() # Add this line to adjust layout
|
336 |
st.pyplot(plt)
|
@@ -339,62 +366,103 @@ def perform_analysis(uploaded_dataframes):
|
|
339 |
pass
|
340 |
|
341 |
def display_story_points_stats(force_refresh=False):
|
342 |
-
"""Display story points statistics from current sprint"""
|
343 |
if not st.session_state.jira_client:
|
344 |
return
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
349 |
board = get_regression_board("RS")
|
350 |
if not board:
|
351 |
return
|
352 |
-
|
353 |
-
# Get current sprint
|
354 |
sprint = get_current_sprint(board['id'])
|
355 |
if not sprint:
|
356 |
return
|
357 |
-
|
358 |
-
# Get sprint issues
|
359 |
issues = get_sprint_issues(board['id'], sprint.id, board['estimation_field'])
|
360 |
if not issues:
|
361 |
return
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
398 |
|
399 |
def show_task_creation_section(filtered_df, environment):
|
400 |
"""Display the task creation section with detailed functional area mapping information."""
|
@@ -475,39 +543,9 @@ def multiple_main():
|
|
475 |
if 'filtered_scenarios_df' not in st.session_state:
|
476 |
st.session_state.filtered_scenarios_df = None
|
477 |
|
478 |
-
if 'jira_server' not in st.session_state:
|
479 |
-
st.session_state.jira_server = JIRA_SERVER
|
480 |
-
# Initialize session state for sprint data if not exists
|
481 |
if 'sprint_data_initialized' not in st.session_state:
|
482 |
st.session_state.sprint_data_initialized = False
|
483 |
|
484 |
-
# Add Jira login to sidebar (only once)
|
485 |
-
with st.sidebar:
|
486 |
-
st.subheader("Jira Integration (Optional)")
|
487 |
-
|
488 |
-
# Only render login if not already authenticated
|
489 |
-
if 'is_authenticated' not in st.session_state:
|
490 |
-
st.session_state.is_authenticated = render_jira_login()
|
491 |
-
else:
|
492 |
-
# Just display the status without re-rendering the login
|
493 |
-
if st.session_state.is_authenticated:
|
494 |
-
st.success("Connected to Jira")
|
495 |
-
else:
|
496 |
-
# Allow re-login if not authenticated
|
497 |
-
st.session_state.is_authenticated = render_jira_login()
|
498 |
-
|
499 |
-
# Only show story points in sidebar if authenticated
|
500 |
-
if st.session_state.is_authenticated and st.session_state.jira_client:
|
501 |
-
st.markdown("---")
|
502 |
-
st.subheader("Sprint Progress")
|
503 |
-
|
504 |
-
# Only fetch sprint data once or when refresh is clicked
|
505 |
-
if not st.session_state.sprint_data_initialized:
|
506 |
-
display_story_points_stats(force_refresh=True)
|
507 |
-
st.session_state.sprint_data_initialized = True
|
508 |
-
else:
|
509 |
-
display_story_points_stats(force_refresh=False)
|
510 |
-
|
511 |
st.title("Multiple File Analysis")
|
512 |
|
513 |
# Initialize session state for uploaded data
|
|
|
25 |
from dotenv import load_dotenv
|
26 |
import json
|
27 |
import logging
|
28 |
+
|
29 |
+
# Inject CSS to shrink metric font sizes and padding to prevent ellipsis overflow
|
30 |
+
if __name__ == "__main__":
|
31 |
+
st.markdown("""
|
32 |
+
<style>
|
33 |
+
[data-testid="metric-container"] {
|
34 |
+
padding: 0.25rem 0.5rem !important;
|
35 |
+
min-width: 80px !important;
|
36 |
+
overflow: visible !important;
|
37 |
+
}
|
38 |
+
[data-testid="metric-container"] div {
|
39 |
+
white-space: nowrap !important;
|
40 |
+
text-overflow: clip !important;
|
41 |
+
}
|
42 |
+
[data-testid="metric-value"] {
|
43 |
+
font-size: 0.8rem !important;
|
44 |
+
}
|
45 |
+
[data-testid="metric-label"] {
|
46 |
+
font-size: 0.6rem !important;
|
47 |
+
}
|
48 |
+
</style>
|
49 |
+
""", unsafe_allow_html=True)
|
50 |
+
|
51 |
load_dotenv()
|
52 |
JIRA_SERVER = os.getenv("JIRA_SERVER")
|
53 |
# Initialize session state variables
|
|
|
260 |
grouped_filtered_scenarios.index = range(1, len(grouped_filtered_scenarios) + 1)
|
261 |
st.dataframe(grouped_filtered_scenarios)
|
262 |
|
263 |
+
# Task creation section: always show button placeholder with tooltip, enabling only when conditions are met
|
264 |
+
can_create_task = (
|
265 |
+
'jira_client' in st.session_state and
|
|
|
|
|
266 |
st.session_state.jira_client and
|
267 |
selected_status == 'Failed' and
|
268 |
len(selected_functional_areas) == 1 and
|
269 |
+
"All" not in selected_functional_areas
|
270 |
+
)
|
271 |
+
col1, col2, col3 = st.columns([1, 2, 1])
|
272 |
+
with col2:
|
273 |
if st.session_state.show_success and st.session_state.last_task_key:
|
274 |
st.success("β
Task created successfully!")
|
|
|
|
|
275 |
st.markdown(
|
276 |
f"""
|
277 |
<div style='padding: 10px; border-radius: 5px; border: 1px solid #90EE90; margin: 10px 0;'>
|
|
|
286 |
""",
|
287 |
unsafe_allow_html=True
|
288 |
)
|
289 |
+
if st.button("Create Another Task", key="create_another", use_container_width=True):
|
290 |
+
st.session_state.task_content = None
|
291 |
+
st.session_state.last_task_key = None
|
292 |
+
st.session_state.last_task_url = None
|
293 |
+
st.session_state.show_success = False
|
294 |
+
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
295 |
else:
|
296 |
+
help_text = (
|
297 |
+
"Requires: Jira login, 'Failed' status selected, "
|
298 |
+
"and exactly one functional area (not 'All')."
|
299 |
+
)
|
300 |
+
if st.button(
|
301 |
+
"π Log Jira Task",
|
302 |
+
disabled=not can_create_task,
|
303 |
+
use_container_width=True,
|
304 |
+
help=help_text
|
305 |
+
) and can_create_task:
|
306 |
+
environment = filtered_scenarios['Environment'].iloc[0]
|
307 |
+
task_df = grouped_filtered_scenarios.copy()
|
308 |
+
expected_columns = [
|
309 |
+
'Environment',
|
310 |
+
'Functional area',
|
311 |
+
'Scenario Name',
|
312 |
+
'Error Message',
|
313 |
+
'Failed Step',
|
314 |
+
'Time spent(m:s)',
|
315 |
+
'Start datetime'
|
316 |
+
]
|
317 |
+
missing_columns = [col for col in expected_columns if col not in task_df.columns]
|
318 |
+
if missing_columns:
|
319 |
+
st.error(f"Missing required columns: {', '.join(missing_columns)}")
|
320 |
+
st.error("Please ensure your data includes all required columns")
|
321 |
+
return
|
322 |
+
summary, description = generate_task_content(task_df)
|
323 |
+
if summary and description:
|
324 |
+
handle_task_button_click(summary, description, environment, task_df)
|
325 |
|
326 |
# Check if selected_status is 'Failed' and show bar graph
|
327 |
if selected_status != 'Passed':
|
|
|
345 |
# Display individual numbers on y-axis
|
346 |
for bar in bars:
|
347 |
height = bar.get_height()
|
348 |
+
# Annotate bar height, defaulting to 0 if conversion fails
|
349 |
+
try:
|
350 |
+
# Ensure numeric conversion in case of string 'NaN'
|
351 |
+
h_int = int(float(height))
|
352 |
+
except Exception:
|
353 |
+
h_int = 0
|
354 |
+
plt.text(
|
355 |
+
bar.get_x() + bar.get_width() / 2,
|
356 |
+
height,
|
357 |
+
str(h_int),
|
358 |
+
ha='center',
|
359 |
+
va='bottom'
|
360 |
+
) # Reduce font size of individual numbers
|
361 |
|
362 |
plt.tight_layout() # Add this line to adjust layout
|
363 |
st.pyplot(plt)
|
|
|
366 |
pass
|
367 |
|
368 |
def display_story_points_stats(force_refresh=False):
|
369 |
+
"""Display story points statistics from current sprint with caching"""
|
370 |
if not st.session_state.jira_client:
|
371 |
return
|
372 |
+
|
373 |
+
# Initialize cache
|
374 |
+
if 'sprint_data_cache' not in st.session_state:
|
375 |
+
st.session_state.sprint_data_cache = None
|
376 |
+
if 'last_sprint_fetch' not in st.session_state:
|
377 |
+
st.session_state.last_sprint_fetch = None
|
378 |
+
|
379 |
+
now = datetime.now()
|
380 |
+
cache_expiry = 300 # 5 minutes
|
381 |
+
refresh_needed = (
|
382 |
+
force_refresh
|
383 |
+
or st.session_state.sprint_data_cache is None
|
384 |
+
or (st.session_state.last_sprint_fetch
|
385 |
+
and (now - st.session_state.last_sprint_fetch).total_seconds() > cache_expiry)
|
386 |
+
)
|
387 |
+
|
388 |
+
if refresh_needed:
|
389 |
+
if force_refresh:
|
390 |
+
with st.spinner("Fetching sprint data..."):
|
391 |
+
board = get_regression_board("RS")
|
392 |
+
if not board:
|
393 |
+
return
|
394 |
+
sprint = get_current_sprint(board['id'])
|
395 |
+
if not sprint:
|
396 |
+
return
|
397 |
+
issues = get_sprint_issues(board['id'], sprint.id, board['estimation_field'])
|
398 |
+
if not issues:
|
399 |
+
return
|
400 |
+
_, total_points, completed_points, in_progress_points = calculate_points(
|
401 |
+
issues, board['estimation_field']
|
402 |
+
)
|
403 |
+
st.session_state.sprint_data_cache = {
|
404 |
+
'sprint_name': sprint.name,
|
405 |
+
'total_points': total_points,
|
406 |
+
'completed_points': completed_points,
|
407 |
+
'in_progress_points': in_progress_points
|
408 |
+
}
|
409 |
+
st.session_state.last_sprint_fetch = now
|
410 |
+
else:
|
411 |
+
# Fetch data silently without spinner
|
412 |
board = get_regression_board("RS")
|
413 |
if not board:
|
414 |
return
|
|
|
|
|
415 |
sprint = get_current_sprint(board['id'])
|
416 |
if not sprint:
|
417 |
return
|
|
|
|
|
418 |
issues = get_sprint_issues(board['id'], sprint.id, board['estimation_field'])
|
419 |
if not issues:
|
420 |
return
|
421 |
+
_, total_points, completed_points, in_progress_points = calculate_points(
|
422 |
+
issues, board['estimation_field']
|
423 |
+
)
|
424 |
+
st.session_state.sprint_data_cache = {
|
425 |
+
'sprint_name': sprint.name,
|
426 |
+
'total_points': total_points,
|
427 |
+
'completed_points': completed_points,
|
428 |
+
'in_progress_points': in_progress_points
|
429 |
+
}
|
430 |
+
st.session_state.last_sprint_fetch = now
|
431 |
+
|
432 |
+
# Display cached sprint data
|
433 |
+
if st.session_state.sprint_data_cache:
|
434 |
+
sprint_data = st.session_state.sprint_data_cache
|
435 |
+
|
436 |
+
# Use markdown with custom HTML for a compact, non-truncating display
|
437 |
+
metrics_html = f"""
|
438 |
+
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; text-align: center; font-size: 0.8rem;">
|
439 |
+
<div>
|
440 |
+
<div style="color: #888;">Total</div>
|
441 |
+
<div style="font-size: 1rem; font-weight: bold;">{sprint_data['total_points']:.1f}</div>
|
442 |
+
</div>
|
443 |
+
<div>
|
444 |
+
<div style="color: #888;">Done</div>
|
445 |
+
<div style="font-size: 1rem; font-weight: bold;">{sprint_data['completed_points']:.1f}</div>
|
446 |
+
</div>
|
447 |
+
<div>
|
448 |
+
<div style="color: #888;">In Progress</div>
|
449 |
+
<div style="font-size: 1rem; font-weight: bold;">{sprint_data['in_progress_points']:.1f}</div>
|
450 |
+
</div>
|
451 |
+
<div>
|
452 |
+
<div style="color: #888;">Complete</div>
|
453 |
+
<div style="font-size: 1rem; font-weight: bold;">{(
|
454 |
+
sprint_data['completed_points'] / sprint_data['total_points'] * 100
|
455 |
+
if sprint_data['total_points'] > 0 else 0
|
456 |
+
):.1f}%</div>
|
457 |
+
</div>
|
458 |
+
</div>
|
459 |
+
"""
|
460 |
+
st.markdown(metrics_html, unsafe_allow_html=True)
|
461 |
+
|
462 |
+
st.progress(
|
463 |
+
sprint_data['completed_points'] / sprint_data['total_points']
|
464 |
+
if sprint_data['total_points'] > 0 else 0
|
465 |
+
)
|
466 |
|
467 |
def show_task_creation_section(filtered_df, environment):
|
468 |
"""Display the task creation section with detailed functional area mapping information."""
|
|
|
543 |
if 'filtered_scenarios_df' not in st.session_state:
|
544 |
st.session_state.filtered_scenarios_df = None
|
545 |
|
|
|
|
|
|
|
546 |
if 'sprint_data_initialized' not in st.session_state:
|
547 |
st.session_state.sprint_data_initialized = False
|
548 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
549 |
st.title("Multiple File Analysis")
|
550 |
|
551 |
# Initialize session state for uploaded data
|
multiple_env_loader.py
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use default Streamlit page configuration
|
2 |
+
import streamlit as st
|
3 |
+
|
4 |
+
# Configure the Streamlit app to use a wide layout for maximum content width
|
5 |
+
st.set_page_config(layout="wide")
|
6 |
+
|
7 |
+
def main():
|
8 |
+
from pathlib import Path
|
9 |
+
from datetime import datetime, timedelta
|
10 |
+
from pre import preprocess_uploaded_file
|
11 |
+
from environment_loader import get_environments_paths
|
12 |
+
from multiple import perform_analysis
|
13 |
+
|
14 |
+
# Sidebar: Environment selection and loading options
|
15 |
+
st.sidebar.header("Auto Environment Loader")
|
16 |
+
base_path = st.sidebar.text_input("Base folder path", "Q:/Selenium_Reports")
|
17 |
+
|
18 |
+
# Dynamically discover available environment folders
|
19 |
+
env_root = Path(base_path)
|
20 |
+
try:
|
21 |
+
env_codes = [p.name for p in env_root.iterdir() if p.is_dir()]
|
22 |
+
except Exception:
|
23 |
+
env_codes = []
|
24 |
+
st.sidebar.error(f"Could not list environments under {base_path}")
|
25 |
+
|
26 |
+
selected_envs = st.sidebar.multiselect("Select environments to load", env_codes)
|
27 |
+
# For envs ending with 'FIN', let user pick one of the 3 most recent files
|
28 |
+
version_selection = {}
|
29 |
+
for env in selected_envs:
|
30 |
+
# Only FIN environments get manual version selection
|
31 |
+
if env.upper().endswith('FIN'):
|
32 |
+
folder = Path(base_path) / env / 'XLSX'
|
33 |
+
if not folder.exists():
|
34 |
+
st.sidebar.warning(f"No folder found for environment {env}: {folder}")
|
35 |
+
continue
|
36 |
+
# List the 3 most recent files
|
37 |
+
recent_files = sorted(folder.glob('*.xlsx'), key=lambda f: f.stat().st_mtime, reverse=True)[:3]
|
38 |
+
if not recent_files:
|
39 |
+
st.sidebar.warning(f"No .xlsx files found for environment {env}")
|
40 |
+
continue
|
41 |
+
labels = [f.name for f in recent_files]
|
42 |
+
choice = st.sidebar.selectbox(f"Select version for {env}", labels, index=0, key=f"select_{env}")
|
43 |
+
version_selection[env] = folder / choice
|
44 |
+
|
45 |
+
# Initialize session state for incremental loading
|
46 |
+
if 'loaded_envs' not in st.session_state:
|
47 |
+
st.session_state.loaded_envs = []
|
48 |
+
if 'uploaded_env_data' not in st.session_state:
|
49 |
+
st.session_state.uploaded_env_data = {}
|
50 |
+
|
51 |
+
if st.sidebar.button("Load Latest Files", use_container_width=True):
|
52 |
+
if not selected_envs:
|
53 |
+
st.sidebar.warning("Please select at least one environment to load.")
|
54 |
+
else:
|
55 |
+
# Determine which environments are new
|
56 |
+
new_envs = [env for env in selected_envs if env not in st.session_state.loaded_envs]
|
57 |
+
if not new_envs:
|
58 |
+
st.sidebar.info("β
All selected environments are already loaded.")
|
59 |
+
return
|
60 |
+
# Build paths: manual picks for FIN envs, auto for others
|
61 |
+
paths = {}
|
62 |
+
missing = []
|
63 |
+
fin_envs = [env for env in new_envs if env.upper().endswith('FIN')]
|
64 |
+
non_fin = [env for env in new_envs if not env.upper().endswith('FIN')]
|
65 |
+
# FIN envs must have manual selection
|
66 |
+
for env in fin_envs:
|
67 |
+
path = version_selection.get(env)
|
68 |
+
if not path:
|
69 |
+
missing.append(env)
|
70 |
+
else:
|
71 |
+
paths[env] = path
|
72 |
+
if missing:
|
73 |
+
st.sidebar.error(f"Please select a file version for: {', '.join(missing)}")
|
74 |
+
return
|
75 |
+
# Auto-load latest for non-FIN envs
|
76 |
+
if non_fin:
|
77 |
+
auto_paths = get_environments_paths(non_fin, base_path=base_path)
|
78 |
+
paths.update(auto_paths)
|
79 |
+
try:
|
80 |
+
dfs = []
|
81 |
+
loaded = []
|
82 |
+
failed = []
|
83 |
+
# Preprocess each new file, tracking success/failure
|
84 |
+
for env, path in paths.items():
|
85 |
+
try:
|
86 |
+
df = preprocess_uploaded_file(path)
|
87 |
+
dfs.append(df)
|
88 |
+
loaded.append((env, path))
|
89 |
+
except Exception as e:
|
90 |
+
failed.append((env, path, e))
|
91 |
+
|
92 |
+
# Show ticks for successful and failed loads
|
93 |
+
if loaded:
|
94 |
+
st.sidebar.markdown("**β
Successfully loaded:**")
|
95 |
+
for env, path in loaded:
|
96 |
+
st.sidebar.markdown(f"- β
{env}: `{path}`")
|
97 |
+
if failed:
|
98 |
+
st.sidebar.markdown("**β Failed to load:**")
|
99 |
+
for env, path, e in failed:
|
100 |
+
st.sidebar.markdown(f"- β {env}: `{path}` ({e})")
|
101 |
+
|
102 |
+
# Update session state with new loaded data
|
103 |
+
for (env, _), df in zip(loaded, dfs):
|
104 |
+
st.session_state.uploaded_env_data[env] = df
|
105 |
+
st.session_state.loaded_envs.append(env)
|
106 |
+
# Build combined list of DataFrames for analysis
|
107 |
+
st.session_state.uploaded_data = list(st.session_state.uploaded_env_data.values())
|
108 |
+
except Exception as e:
|
109 |
+
st.sidebar.error(str(e))
|
110 |
+
|
111 |
+
# Main: run analysis if data is loaded
|
112 |
+
st.title("Multiple File Analysis (Auto Env Loader)")
|
113 |
+
if 'uploaded_data' in st.session_state and st.session_state.uploaded_data:
|
114 |
+
perform_analysis(st.session_state.uploaded_data)
|
115 |
+
else:
|
116 |
+
st.info("Use the sidebar to select environments and load their latest files for analysis.")
|
117 |
+
|
118 |
+
|
119 |
+
if __name__ == "__main__":
|
120 |
+
main()
|
pre.py
CHANGED
@@ -43,18 +43,28 @@ def preprocess_xlsx(uploaded_file):
|
|
43 |
'Failed Scenario': 'string'
|
44 |
}
|
45 |
|
46 |
-
#
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
|
59 |
# Print column names and sample values for debugging
|
60 |
# st.write("Excel columns:", df.columns.tolist())
|
@@ -66,9 +76,13 @@ def preprocess_xlsx(uploaded_file):
|
|
66 |
# empty_features = df['Feature Name'].isna().sum()
|
67 |
# st.write(f"Empty Feature Names: {empty_features}")
|
68 |
|
69 |
-
# Convert
|
70 |
-
|
71 |
df['Failed Scenario'] = df['Failed Scenario'].astype(str).str.upper()
|
|
|
|
|
|
|
|
|
72 |
df['Status'] = df['Failed Scenario'].map(
|
73 |
lambda x: 'FAILED' if x in ['TRUE', 'YES', 'Y', '1'] else 'PASSED'
|
74 |
)
|
@@ -132,8 +146,9 @@ def preprocess_xlsx(uploaded_file):
|
|
132 |
# Add environment column
|
133 |
result_df['Environment'] = environment
|
134 |
|
135 |
-
# Calculate formatted time spent
|
136 |
-
|
|
|
137 |
|
138 |
|
139 |
result_df['Start datetime'] = pd.to_datetime(file_date)
|
@@ -154,8 +169,14 @@ def preprocess_xlsx(uploaded_file):
|
|
154 |
result_df = result_df.merge(start_times, on='Scenario Name', how='left')
|
155 |
result_df.rename(columns={'Time Stamp': 'Scenario Start Time'}, inplace=True)
|
156 |
scenario_start_times = result_df['Scenario Start Time']
|
157 |
-
# Combine
|
158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
159 |
|
160 |
# Print counts for debugging
|
161 |
# st.write(f"Processed data - Failed: {len(result_df[result_df['Status'] == 'FAILED'])}, Passed: {len(result_df[result_df['Status'] == 'PASSED'])}")
|
@@ -194,7 +215,9 @@ def preprocess_uploaded_file(uploaded_file):
|
|
194 |
data['Start datetime'] = pd.to_datetime(data['Start datetime'], dayfirst=True, errors='coerce')
|
195 |
data['End datetime'] = pd.to_datetime(data['End datetime'], dayfirst=True, errors='coerce')
|
196 |
data['Time spent'] = (data['End datetime'] - data['Start datetime']).dt.total_seconds()
|
197 |
-
|
|
|
|
|
198 |
|
199 |
# Extract environment name from filename
|
200 |
filename = uploaded_file.name
|
|
|
43 |
'Failed Scenario': 'string'
|
44 |
}
|
45 |
|
46 |
+
# Attempt fast streaming read (read_only) for performance
|
47 |
+
fast_excel = pd.ExcelFile(uploaded_file, engine='openpyxl')
|
48 |
+
# Read first sheet (error messages)
|
49 |
+
error_df = pd.read_excel(fast_excel, sheet_name=0)
|
50 |
+
# Read 'Time Taken' sheet in fast mode
|
51 |
+
df = pd.read_excel(fast_excel, sheet_name='Time Taken', dtype=object)
|
52 |
+
|
53 |
+
# If the sheet appears truncated (e.g., only header row) or missing expected columns, retry in full mode
|
54 |
+
if df.shape[0] <= 1 or 'Total Time Taken (ms)' not in df.columns:
|
55 |
+
st.warning("Fast Excel read produced incomplete data; retrying in full mode.")
|
56 |
+
slow_excel = pd.ExcelFile(
|
57 |
+
uploaded_file,
|
58 |
+
engine='openpyxl',
|
59 |
+
engine_kwargs={
|
60 |
+
'read_only': False,
|
61 |
+
'data_only': True,
|
62 |
+
'keep_links': False
|
63 |
+
}
|
64 |
+
)
|
65 |
+
# Reload both sheets in full mode
|
66 |
+
error_df = pd.read_excel(slow_excel, sheet_name=0)
|
67 |
+
df = pd.read_excel(slow_excel, sheet_name='Time Taken', dtype=object)
|
68 |
|
69 |
# Print column names and sample values for debugging
|
70 |
# st.write("Excel columns:", df.columns.tolist())
|
|
|
76 |
# empty_features = df['Feature Name'].isna().sum()
|
77 |
# st.write(f"Empty Feature Names: {empty_features}")
|
78 |
|
79 |
+
# Convert specific columns after reading as object
|
80 |
+
df['Total Time Taken (ms)'] = pd.to_numeric(df['Total Time Taken (ms)'], errors='coerce').fillna(0).astype('float64')
|
81 |
df['Failed Scenario'] = df['Failed Scenario'].astype(str).str.upper()
|
82 |
+
# Ensure Feature Name and Scenario Name are strings, handling potential NaNs read as objects
|
83 |
+
df['Feature Name'] = df['Feature Name'].astype(str).fillna('Unknown')
|
84 |
+
df['Scenario Name'] = df['Scenario Name'].astype(str)
|
85 |
+
|
86 |
df['Status'] = df['Failed Scenario'].map(
|
87 |
lambda x: 'FAILED' if x in ['TRUE', 'YES', 'Y', '1'] else 'PASSED'
|
88 |
)
|
|
|
146 |
# Add environment column
|
147 |
result_df['Environment'] = environment
|
148 |
|
149 |
+
# Calculate formatted time spent (coerce non-numeric values)
|
150 |
+
_secs = pd.to_numeric(result_df['Time spent'], errors='coerce')
|
151 |
+
result_df['Time spent(m:s)'] = pd.to_datetime(_secs, unit='s', errors='coerce').dt.strftime('%M:%S')
|
152 |
|
153 |
|
154 |
result_df['Start datetime'] = pd.to_datetime(file_date)
|
|
|
169 |
result_df = result_df.merge(start_times, on='Scenario Name', how='left')
|
170 |
result_df.rename(columns={'Time Stamp': 'Scenario Start Time'}, inplace=True)
|
171 |
scenario_start_times = result_df['Scenario Start Time']
|
172 |
+
# Combine time and date strings, then parse with explicit format to prevent mismatches
|
173 |
+
combined = scenario_start_times.dt.strftime('%H:%M:%S') + ' ' + file_date.strftime('%Y-%m-%d')
|
174 |
+
# Let pandas infer the datetime format and coerce invalid parses
|
175 |
+
result_df['Start datetime'] = pd.to_datetime(
|
176 |
+
combined,
|
177 |
+
infer_datetime_format=True,
|
178 |
+
errors='coerce'
|
179 |
+
)
|
180 |
|
181 |
# Print counts for debugging
|
182 |
# st.write(f"Processed data - Failed: {len(result_df[result_df['Status'] == 'FAILED'])}, Passed: {len(result_df[result_df['Status'] == 'PASSED'])}")
|
|
|
215 |
data['Start datetime'] = pd.to_datetime(data['Start datetime'], dayfirst=True, errors='coerce')
|
216 |
data['End datetime'] = pd.to_datetime(data['End datetime'], dayfirst=True, errors='coerce')
|
217 |
data['Time spent'] = (data['End datetime'] - data['Start datetime']).dt.total_seconds()
|
218 |
+
## Format time spent for CSV branch (coerce non-numeric values)
|
219 |
+
_secs = pd.to_numeric(data['Time spent'], errors='coerce')
|
220 |
+
data['Time spent(m:s)'] = pd.to_datetime(_secs, unit='s', errors='coerce').dt.strftime('%M:%S')
|
221 |
|
222 |
# Extract environment name from filename
|
223 |
filename = uploaded_file.name
|