Refactor project structure and add new features

This commit is contained in:
Serge Zaigraeff
2025-10-23 00:42:09 +03:00
parent 144414fcf8
commit 852f7975b9
63 changed files with 8101 additions and 1341 deletions
+576
View File
@@ -0,0 +1,576 @@
{
"expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations",
"id": "494944",
"self": "https://jira.crazypanda.ru/rest/api/2/issue/494944",
"key": "WPCMU-9082",
"fields": {
"customfield_19200": null,
"customfield_18113": null,
"customfield_46700": null,
"customfield_44400": null,
"customfield_15400": null,
"customfield_17700": null,
"customfield_38400": null,
"customfield_18115": null,
"resolution": {
"self": "https://jira.crazypanda.ru/rest/api/2/resolution/6",
"id": "6",
"description": "the task is done",
"name": "Done"
},
"customfield_10500": null,
"customfield_36104": null,
"customfield_36103": null,
"customfield_36102": null,
"customfield_24001": null,
"customfield_45602": null,
"customfield_22601": null,
"customfield_47900": null,
"lastViewed": null,
"customfield_45501": {
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/20611",
"value": "Desktop",
"id": "20611",
"disabled": false
},
"customfield_47001": null,
"customfield_16600": null,
"customfield_14300": null,
"customfield_12000": "0|i0jh73:",
"customfield_39500": null,
"customfield_14301": null,
"customfield_47000": null,
"customfield_14304": null,
"customfield_32305": null,
"customfield_14302": null,
"customfield_16601": null,
"customfield_18107": null,
"customfield_14303": null,
"labels": [],
"aggregatetimeoriginalestimate": null,
"issuelinks": [
{
"id": "485247",
"self": "https://jira.crazypanda.ru/rest/api/2/issueLink/485247",
"type": {
"id": "10010",
"name": "1 Зависит от таска",
"inward": "Parent task",
"outward": "Child task",
"self": "https://jira.crazypanda.ru/rest/api/2/issueLinkType/10010"
},
"inwardIssue": {
"id": "494942",
"key": "WPCMU-9081",
"self": "https://jira.crazypanda.ru/rest/api/2/issue/494942",
"fields": {
"summary": "SLOTS-2",
"status": {
"self": "https://jira.crazypanda.ru/rest/api/2/status/6",
"description": "The issue is considered finished, the resolution is correct. Issues which are closed can be reopened.",
"iconUrl": "https://jira.crazypanda.ru/images/icons/statuses/closed.png",
"name": "Closed",
"id": "6",
"statusCategory": {
"self": "https://jira.crazypanda.ru/rest/api/2/statuscategory/3",
"id": 3,
"key": "done",
"colorName": "green",
"name": "Done"
}
},
"issuetype": {
"self": "https://jira.crazypanda.ru/rest/api/2/issuetype/7",
"id": "7",
"description": "Created by Jira Software - do not edit or delete. Issue type for a user story.",
"iconUrl": "https://jira.crazypanda.ru/secure/viewavatar?size=xsmall&avatarId=16435&avatarType=issuetype",
"name": "Story",
"subtask": false,
"avatarId": 16435
}
}
}
},
{
"id": "502299",
"self": "https://jira.crazypanda.ru/rest/api/2/issueLink/502299",
"type": {
"id": "11620",
"name": "Mention",
"inward": "is mentioned by",
"outward": "mentions",
"self": "https://jira.crazypanda.ru/rest/api/2/issueLinkType/11620"
},
"outwardIssue": {
"id": "503922",
"key": "WPCMU-9682",
"self": "https://jira.crazypanda.ru/rest/api/2/issue/503922",
"fields": {
"summary": "LOC: SLOTS-2",
"status": {
"self": "https://jira.crazypanda.ru/rest/api/2/status/5",
"description": "Story has been released less then 1.5m ago.",
"iconUrl": "https://jira.crazypanda.ru/images/icons/statuses/generic.png",
"name": "Resolved",
"id": "5",
"statusCategory": {
"self": "https://jira.crazypanda.ru/rest/api/2/statuscategory/3",
"id": 3,
"key": "done",
"colorName": "green",
"name": "Done"
}
},
"issuetype": {
"self": "https://jira.crazypanda.ru/rest/api/2/issuetype/3",
"id": "3",
"description": "A task that needs to be done.",
"iconUrl": "https://jira.crazypanda.ru/secure/viewavatar?size=xsmall&avatarId=16438&avatarType=issuetype",
"name": "Task",
"subtask": false,
"avatarId": 16438
}
}
}
}
],
"assignee": {
"self": "https://jira.crazypanda.ru/rest/api/2/user?username=s.zaigraev",
"name": "s.zaigraev",
"key": "s.zaigraev",
"emailAddress": "s.zaigraev@crazypandagames.com",
"avatarUrls": {
"48x48": "https://jira.crazypanda.ru/secure/useravatar?ownerId=s.zaigraev&avatarId=35220",
"24x24": "https://jira.crazypanda.ru/secure/useravatar?size=small&ownerId=s.zaigraev&avatarId=35220",
"16x16": "https://jira.crazypanda.ru/secure/useravatar?size=xsmall&ownerId=s.zaigraev&avatarId=35220",
"32x32": "https://jira.crazypanda.ru/secure/useravatar?size=medium&ownerId=s.zaigraev&avatarId=35220"
},
"displayName": "Заиграев Сергей",
"active": true,
"timeZone": "Europe/Moscow"
},
"customfield_46702": null,
"customfield_46701": null,
"customfield_42901": null,
"customfield_42900": null,
"customfield_23601": null,
"customfield_19300": null,
"customfield_44300": null,
"components": [],
"customfield_44301": null,
"customfield_46600": null,
"customfield_17003": null,
"customfield_17002": null,
"customfield_17001": null,
"customfield_17000": null,
"customfield_15500": null,
"customfield_17004": null,
"customfield_15503": null,
"customfield_15501": null,
"customfield_45502": {
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/20613",
"value": "2",
"id": "20613",
"disabled": false
},
"customfield_45400": null,
"customfield_43100": null,
"customfield_47701": null,
"customfield_43101": null,
"customfield_18201": null,
"customfield_43102": null,
"customfield_43103": null,
"subtasks": [],
"customfield_39400": null,
"customfield_14400": null,
"reporter": {
"self": "https://jira.crazypanda.ru/rest/api/2/user?username=n.andreenko",
"name": "n.andreenko",
"key": "JIRAUSER64600",
"emailAddress": "n.andreenko@crazypandagames.com",
"avatarUrls": {
"48x48": "https://jira.crazypanda.ru/secure/useravatar?avatarId=27235",
"24x24": "https://jira.crazypanda.ru/secure/useravatar?size=small&avatarId=27235",
"16x16": "https://jira.crazypanda.ru/secure/useravatar?size=xsmall&avatarId=27235",
"32x32": "https://jira.crazypanda.ru/secure/useravatar?size=medium&avatarId=27235"
},
"displayName": "Андреенко Наталья",
"active": true,
"timeZone": "Europe/Moscow"
},
"customfield_14401": null,
"customfield_16700": null,
"customfield_40507": null,
"progress": {
"progress": 0,
"total": 0
},
"votes": {
"self": "https://jira.crazypanda.ru/rest/api/2/issue/WPCMU-9082/votes",
"votes": 0,
"hasVoted": false
},
"worklog": {
"startAt": 0,
"maxResults": 20,
"total": 0,
"worklogs": []
},
"customfield_44303": null,
"customfield_42802": null,
"customfield_44305": null,
"customfield_42801": null,
"customfield_46500": null,
"issuetype": {
"self": "https://jira.crazypanda.ru/rest/api/2/issuetype/3",
"id": "3",
"description": "A task that needs to be done.",
"iconUrl": "https://jira.crazypanda.ru/secure/viewavatar?size=xsmall&avatarId=16438&avatarType=issuetype",
"name": "Task",
"subtask": false,
"avatarId": 16438
},
"customfield_19400": null,
"customfield_46502": null,
"customfield_19401": null,
"customfield_46501": null,
"project": {
"self": "https://jira.crazypanda.ru/rest/api/2/project/15550",
"id": "15550",
"key": "WPCMU",
"name": "World Poker Club Unity Mobile",
"projectTypeKey": "software",
"avatarUrls": {
"48x48": "https://jira.crazypanda.ru/secure/projectavatar?avatarId=27223",
"24x24": "https://jira.crazypanda.ru/secure/projectavatar?size=small&avatarId=27223",
"16x16": "https://jira.crazypanda.ru/secure/projectavatar?size=xsmall&avatarId=27223",
"32x32": "https://jira.crazypanda.ru/secure/projectavatar?size=medium&avatarId=27223"
}
},
"customfield_15602": null,
"customfield_17900": null,
"customfield_15600": null,
"customfield_15601": null,
"customfield_34401": null,
"customfield_16804": null,
"customfield_16803": null,
"resolutiondate": "2025-08-29T17:43:43.998+0300",
"customfield_41600": null,
"customfield_43901": null,
"customfield_22801": null,
"customfield_43104": null,
"customfield_43105": [],
"customfield_43106": null,
"customfield_43107": null,
"customfield_45300": null,
"watches": {
"self": "https://jira.crazypanda.ru/rest/api/2/issue/WPCMU-9082/watchers",
"watchCount": 0,
"isWatching": false
},
"customfield_18300": null,
"customfield_18301": null,
"customfield_16002": null,
"customfield_16001": null,
"customfield_16000": null,
"customfield_49102": 8135645407,
"customfield_39300": null,
"customfield_49103": "shortFormatDuration({issue.cf49102})",
"customfield_49100": "2025-05-27T13:49:38.366+0300",
"customfield_16005": 100.0,
"customfield_16004": null,
"customfield_16003": null,
"customfield_18302": null,
"customfield_16802": null,
"customfield_16801": null,
"customfield_14500": null,
"customfield_46507": null,
"customfield_46509": null,
"customfield_46504": null,
"customfield_46503": null,
"updated": "2025-10-07T17:12:22.404+0300",
"customfield_23401": null,
"customfield_46506": null,
"customfield_46505": null,
"customfield_23203": null,
"customfield_46400": null,
"customfield_23202": null,
"customfield_23201": null,
"customfield_19500": null,
"customfield_17200": null,
"timeoriginalestimate": null,
"description": "Ссылка на локу: WPCMU-9682",
"customfield_11100": null,
"customfield_15701": null,
"timetracking": {},
"customfield_15700": null,
"customfield_10005": null,
"customfield_41500": null,
"summary": "GD: SLOTS-2",
"customfield_47500": [],
"customfield_47501": [],
"customfield_16100": null,
"customfield_45200": null,
"customfield_49000": null,
"customfield_18401": null,
"customfield_18402": null,
"customfield_16900": null,
"customfield_18403": null,
"customfield_18404": null,
"customfield_14600": null,
"duedate": "2025-06-06",
"customfield_44902": null,
"comment": {
"comments": [
{
"self": "https://jira.crazypanda.ru/rest/api/2/issue/494944/comment/598612",
"id": "598612",
"author": {
"self": "https://jira.crazypanda.ru/rest/api/2/user?username=s.zaigraev",
"name": "s.zaigraev",
"key": "s.zaigraev",
"emailAddress": "s.zaigraev@crazypandagames.com",
"avatarUrls": {
"48x48": "https://jira.crazypanda.ru/secure/useravatar?ownerId=s.zaigraev&avatarId=35220",
"24x24": "https://jira.crazypanda.ru/secure/useravatar?size=small&ownerId=s.zaigraev&avatarId=35220",
"16x16": "https://jira.crazypanda.ru/secure/useravatar?size=xsmall&ownerId=s.zaigraev&avatarId=35220",
"32x32": "https://jira.crazypanda.ru/secure/useravatar?size=medium&ownerId=s.zaigraev&avatarId=35220"
},
"displayName": "Заиграев Сергей",
"active": true,
"timeZone": "Europe/Moscow"
},
"body": "Наборы слотов: https://confluence.crazypanda.ru/pages/viewpage.action?pageId=1458307128\r\nСписок механик (см дочерние страницы): https://confluence.crazypanda.ru/pages/viewpage.action?pageId=1352728643",
"updateAuthor": {
"self": "https://jira.crazypanda.ru/rest/api/2/user?username=s.zaigraev",
"name": "s.zaigraev",
"key": "s.zaigraev",
"emailAddress": "s.zaigraev@crazypandagames.com",
"avatarUrls": {
"48x48": "https://jira.crazypanda.ru/secure/useravatar?ownerId=s.zaigraev&avatarId=35220",
"24x24": "https://jira.crazypanda.ru/secure/useravatar?size=small&ownerId=s.zaigraev&avatarId=35220",
"16x16": "https://jira.crazypanda.ru/secure/useravatar?size=xsmall&ownerId=s.zaigraev&avatarId=35220",
"32x32": "https://jira.crazypanda.ru/secure/useravatar?size=medium&ownerId=s.zaigraev&avatarId=35220"
},
"displayName": "Заиграев Сергей",
"active": true,
"timeZone": "Europe/Moscow"
},
"created": "2025-06-06T21:41:52.591+0300",
"updated": "2025-06-06T21:41:52.591+0300"
}
],
"maxResults": 1,
"total": 1,
"startAt": 0
},
"customfield_44903": null,
"customfield_23301": null,
"customfield_17300": null,
"customfield_46300": null,
"customfield_44001": null,
"customfield_17302": null,
"customfield_17301": null,
"fixVersions": [],
"customfield_19600": null,
"customfield_38800": null,
"customfield_13501": null,
"customfield_14702": null,
"customfield_14703": null,
"customfield_41400": null,
"customfield_43700": null,
"customfield_45103": null,
"customfield_47400": null,
"customfield_45100": null,
"customfield_10100": "9223372036854775807",
"customfield_14700": null,
"customfield_12400": null,
"customfield_14701": null,
"customfield_15902": null,
"timeestimate": null,
"versions": [],
"customfield_40202": null,
"customfield_40200": null,
"customfield_42500": "{}",
"customfield_40201": null,
"status": {
"self": "https://jira.crazypanda.ru/rest/api/2/status/5",
"description": "Story has been released less then 1.5m ago.",
"iconUrl": "https://jira.crazypanda.ru/images/icons/statuses/generic.png",
"name": "Resolved",
"id": "5",
"statusCategory": {
"self": "https://jira.crazypanda.ru/rest/api/2/statuscategory/3",
"id": 3,
"key": "done",
"colorName": "green",
"name": "Done"
}
},
"customfield_40100": null,
"customfield_15100": null,
"customfield_46200": null,
"customfield_15101": null,
"customfield_15102": null,
"customfield_17400": null,
"customfield_38700": null,
"customfield_19700": "{}",
"aggregatetimeestimate": null,
"customfield_14806": null,
"customfield_45902": null,
"customfield_45901": null,
"customfield_47302": null,
"creator": {
"self": "https://jira.crazypanda.ru/rest/api/2/user?username=n.andreenko",
"name": "n.andreenko",
"key": "JIRAUSER64600",
"emailAddress": "n.andreenko@crazypandagames.com",
"avatarUrls": {
"48x48": "https://jira.crazypanda.ru/secure/useravatar?avatarId=27235",
"24x24": "https://jira.crazypanda.ru/secure/useravatar?size=small&avatarId=27235",
"16x16": "https://jira.crazypanda.ru/secure/useravatar?size=xsmall&avatarId=27235",
"32x32": "https://jira.crazypanda.ru/secure/useravatar?size=medium&avatarId=27235"
},
"displayName": "Андреенко Наталья",
"active": true,
"timeZone": "Europe/Moscow"
},
"customfield_41200": null,
"customfield_47300": null,
"customfield_45000": null,
"customfield_47301": "<a href=\"https://confluence.crazypanda.ru/x/CgBgO\">https://confluence.crazypanda.ru/x/CgBgO</a>",
"customfield_16300": null,
"aggregateprogress": {
"progress": 0,
"total": 0
},
"customfield_39800": null,
"customfield_18600": null,
"customfield_10200": [
"com.atlassian.greenhopper.service.sprint.Sprint@1f8ba4cd[id=673,rapidViewId=506,state=CLOSED,name=MOB -Team 1- Sprint 188,startDate=2025-05-27T12:57:00.000+03:00,endDate=2025-06-10T12:57:00.000+03:00,completeDate=2025-06-17T11:28:01.892+03:00,activatedDate=2025-05-27T12:57:24.826+03:00,sequence=674,goal=,autoStartStop=false]",
"com.atlassian.greenhopper.service.sprint.Sprint@4e0528d1[id=688,rapidViewId=506,state=CLOSED,name=MOB -Team 1- Sprint 189,startDate=2025-06-16T11:29:00.000+03:00,endDate=2025-06-27T11:29:00.000+03:00,completeDate=2025-06-30T11:08:30.366+03:00,activatedDate=2025-06-17T11:30:29.377+03:00,sequence=688,goal=,autoStartStop=false]",
"com.atlassian.greenhopper.service.sprint.Sprint@68f8b40a[id=692,rapidViewId=506,state=CLOSED,name=MOB -Team 1- Sprint 190,startDate=2025-06-30T11:07:00.000+03:00,endDate=2025-07-11T11:07:00.000+03:00,completeDate=2025-07-14T10:48:35.813+03:00,activatedDate=2025-06-30T11:08:56.638+03:00,sequence=692,goal=,autoStartStop=false]",
"com.atlassian.greenhopper.service.sprint.Sprint@833683d[id=699,rapidViewId=506,state=CLOSED,name=MOB -Team 1- Sprint 191,startDate=2025-07-14T12:06:00.000+03:00,endDate=2025-07-25T12:06:00.000+03:00,completeDate=2025-07-28T11:15:06.279+03:00,activatedDate=2025-07-14T10:48:57.081+03:00,sequence=699,goal=,autoStartStop=false]",
"com.atlassian.greenhopper.service.sprint.Sprint@196c70b9[id=714,rapidViewId=506,state=CLOSED,name=MOB -Team 1- Sprint 192,startDate=2025-07-28T11:14:00.000+03:00,endDate=2025-08-08T11:14:00.000+03:00,completeDate=2025-08-11T10:27:17.098+03:00,activatedDate=2025-07-28T11:16:18.567+03:00,sequence=714,goal=,autoStartStop=false]",
"com.atlassian.greenhopper.service.sprint.Sprint@d6ac476[id=720,rapidViewId=506,state=CLOSED,name=MOB -Team 1- Sprint 193,startDate=2025-08-11T10:25:00.000+03:00,endDate=2025-08-22T10:25:00.000+03:00,completeDate=2025-08-25T17:31:38.356+03:00,activatedDate=2025-08-11T10:28:03.549+03:00,sequence=720,goal=,autoStartStop=false]"
],
"customfield_18601": null,
"customfield_40900": [],
"customfield_19002": null,
"customfield_19003": null,
"customfield_19004": null,
"customfield_40000": null,
"customfield_46100": null,
"customfield_40001": null,
"customfield_19000": null,
"customfield_46102": null,
"timespent": null,
"customfield_19001": null,
"customfield_46101": null,
"customfield_15200": null,
"customfield_17500": {
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/13601",
"value": "No",
"id": "13601",
"disabled": false
},
"customfield_38601": null,
"aggregatetimespent": null,
"customfield_13700": null,
"customfield_19800": null,
"customfield_10302": null,
"customfield_43505": null,
"workratio": -1,
"customfield_43506": null,
"customfield_43507": null,
"customfield_45800": null,
"customfield_43500": null,
"customfield_45801": null,
"customfield_43503": null,
"customfield_49500": null,
"customfield_47202": "<span style=\"color: #ff6600;\">Привет, ты редактируешь Request во внутренние сервисы.<br>\r\nСвободно оставляй тут вопросы, просьбу помощи, баг, запрос на фичу. <br>\r\nПодробное описание, как это работает смотри <a href=\"https://confluence.crazypanda.ru/pages/viewpage.action?pageId=945815562\">тут</a></span></p>",
"created": "2025-05-19T13:10:05.955+0300",
"customfield_47201": null,
"customfield_16402": null,
"customfield_16401": null,
"customfield_16400": null,
"customfield_39700": null,
"customfield_10300": null,
"customfield_10301": null,
"customfield_18701": null,
"customfield_13800": null,
"customfield_42303": null,
"customfield_44600": null,
"customfield_44500": null,
"customfield_46001": null,
"customfield_46000": null,
"customfield_46003": null,
"customfield_19100": null,
"customfield_46002": null,
"customfield_17601": null,
"customfield_17600": null,
"customfield_13000": null,
"customfield_38500": null,
"customfield_15300": null,
"customfield_38501": null,
"customfield_38502": null,
"customfield_19900": "{}",
"attachment": [],
"customfield_45700": null,
"customfield_41900": null,
"customfield_49405": null,
"customfield_46810": null,
"customfield_43300": null,
"customfield_49403": null,
"customfield_24902": null,
"customfield_24903": null,
"customfield_47100": null,
"customfield_41000": null,
"customfield_49400": null,
"customfield_16501": null,
"customfield_18801": null,
"customfield_16500": null,
"customfield_18802": null,
"customfield_18800": null,
"customfield_13900": {
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/11301",
"value": "нет",
"id": "11301",
"disabled": false
},
"customfield_13902": {
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/11307",
"value": "нет",
"id": "11307",
"disabled": false
},
"customfield_13901": {
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/11304",
"value": "нет",
"id": "11304",
"disabled": false
},
"customfield_46809": null,
"customfield_13904": {
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/11314",
"value": "Не требуется",
"id": "11314",
"disabled": false
},
"customfield_46808": null,
"customfield_13903": {
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/11310",
"value": "нет",
"id": "11310",
"disabled": false
},
"customfield_46804": null,
"customfield_46807": null,
"customfield_46806": null,
"customfield_42202": null,
"customfield_46801": null,
"customfield_46800": null,
"customfield_46803": null,
"customfield_46802": null
}
}
+465
View File
@@ -0,0 +1,465 @@
# Integration Plan: Parent Story and Story Points for Global Tasks
## Overview
This document outlines the plan to enhance the `fetch_all_tasks_with_due_dates()` method in `client_jira.py` to include parent story information and story points for each task. This enhancement applies **only** to global tasks that appear in the Tasks report table, not to individual assignee-specific tasks. The enhancement will include a caching mechanism to optimize performance when multiple tasks belong to the same parent story.
## Scope Clarification
**Included in this enhancement:**
- Global tasks fetched via `fetch_all_tasks_with_due_dates()`
- Tasks that appear in the Tasks report table
- Parent story key and storypoints extraction
**Excluded from this enhancement:**
- Individual assignee-specific tasks
- Tasks fetched via other methods
- Other report types
## Current State Analysis
### Current Data Flow
```mermaid
graph TB
subgraph "Current Implementation"
A[fetch_all_tasks_with_due_dates] --> B[_make_search_request]
B --> C[JIRA API]
C --> D[Task Data]
D --> E[format_task_data]
E --> F[Report Generation]
F --> G[Spreadsheet Export]
end
subgraph "Current Task Data Structure"
T1[issue]
T2[start]
T3[resolved]
T4[due_date]
T5[labels]
T6[responsible]
end
```
### Current Task Data Structure
The current task data includes:
- issue: Task key
- start: Creation date
- resolved: Resolution date
- due_date: Due date
- labels: Task labels
- responsible: Responsible person
## Target Architecture
### Enhanced Data Flow
```mermaid
graph TB
subgraph "Enhanced Implementation"
A[fetch_all_tasks_with_due_dates] --> B[_make_search_request]
B --> C[JIRA API]
C --> D[Task Data]
D --> E[Process Parent Stories]
E --> F{Parent Story Cache}
F -->|Cache Hit| G[Use Cached Data]
F -->|Cache Miss| H[Fetch Parent Story]
H --> I[Update Cache]
I --> J[Merge Task + Story Data]
G --> J
J --> K[format_task_data]
K --> L[Report Generation]
L --> M[Spreadsheet Export]
end
subgraph "Enhanced Task Data Structure"
T1[issue]
T2[start]
T3[resolved]
T4[due_date]
T5[labels]
T6[responsible]
T7[parent_story]
T8[storypoints]
end
```
### Parent Story Link Structure
Based on the example JSON (`WPCMU-9082.json`), the parent story information is located in:
- Path: `fields.issuelinks`
- Filter: `type.inward = "Parent task"`
- Parent Key: `inwardIssue.key`
## Implementation Plan
### Phase 1: Core Infrastructure
#### 1.1 Create Parent Story Cache Manager
**File**: `src/api/story_cache.py`
```python
class StoryCache:
"""
Manages caching of parent story data to optimize API calls
when multiple tasks belong to the same parent story.
"""
def __init__(self):
self.cache = {} # {story_key: storypoints}
def get(self, story_key: str) -> Optional[int]:
"""Get storypoints from cache"""
def set(self, story_key: str, storypoints: int) -> None:
"""Set storypoints in cache"""
def clear(self) -> None:
"""Clear the cache"""
```
#### 1.2 Create Parent Story Fetcher
**File**: `src/api/client_jira.py`
```python
def _fetch_parent_story_data(jira_url: str, bearer_token: str, story_key: str) -> Optional[Dict]:
"""
Fetches parent story data including storypoints
Args:
jira_url: URL Jira API
bearer_token: Bearer токен для аутентификации
story_key: Key of the parent story
Returns:
Dictionary with story data or None if error
"""
# Implementation to fetch single story by key
# Extract storypoints from customfield_10003
# Return cached data structure
```
### Phase 2: Integration with Task Fetching
#### 2.1 Modify fetch_all_tasks_with_due_dates
**File**: `src/api/client_jira.py`
```python
def fetch_all_tasks_with_due_dates(
jira_url: str,
bearer_token: str,
start_date: str,
end_date: str,
max_results: int = 50,
story_cache: Optional[StoryCache] = None
) -> Optional[List[Dict]]:
"""
Enhanced version that includes parent story and storypoints data
"""
# Initialize cache if not provided
# Fetch tasks as before
# For each task:
# - Extract parent story key from issuelinks
# - Check cache for storypoints
# - Fetch from API if not in cache
# - Update cache
# - Add parent_story and storypoints to task data
```
#### 2.2 Update Task Data Structure
**File**: `src/formatting/reports_data_formatter.py`
```python
def format_task_data(task_issue: Dict, target_tz: Optional[ZoneInfo]) -> Dict:
"""
Enhanced version that includes parent story and storypoints
"""
# Extract parent story information
parent_story = None
storypoints = None
# Extract from issuelinks where type.inward = "Parent task"
issuelinks = task_issue.get('fields', {}).get('issuelinks', [])
for link in issuelinks:
if link.get('type', {}).get('inward') == 'Parent task':
parent_story = link.get('inwardIssue', {}).get('key')
break
# Get storypoints from the enhanced task data
storypoints = task_issue.get('parent_storypoints')
return {
"issue": issue_key,
"start": formatted_created_date,
"resolved": formatted_resolution_date,
"due_date": formatted_due_date,
"labels": labels_str,
"responsible": formatted_responsible,
"parent_story": parent_story,
"storypoints": storypoints
}
```
### Phase 3: Report Integration
#### 3.1 Update GlobalDataManager with Cache Pre-population
**File**: `src/reports/report_generator.py`
```python
class GlobalDataManager:
def __init__(self, jira_url: str, bearer_token: str):
self.jira_url = jira_url
self.bearer_token = bearer_token
self.cached_bugs = None
self.cached_stories = None
self.cached_tasks = None
self.story_cache = StoryCache() # New cache manager
def _populate_story_cache(self, stories: List[Dict]) -> None:
"""
Pre-populates the story cache with all stories and their storypoints
to optimize task processing later.
Args:
stories: List of story dictionaries from JIRA API
"""
for story in stories:
story_key = story.get('key')
storypoints = story.get('fields', {}).get('customfield_10003')
if story_key and storypoints is not None:
self.story_cache.set(story_key, storypoints)
def fetch_global_data(self, start_date: str, end_date: str, max_results: int = 50) -> Dict:
results = {}
# Get stories first and populate cache
self.cached_stories = fetch_all_stories_with_due_dates(
self.jira_url, self.bearer_token, start_date, end_date, max_results
)
results['stories'] = self.cached_stories or []
# Pre-populate cache with story data for optimization
if self.cached_stories:
self._populate_story_cache(self.cached_stories)
# Get bugs
self.cached_bugs = fetch_all_bugs(
self.jira_url, self.bearer_token, start_date, end_date, max_results
)
results['bugs'] = self.cached_bugs or []
# Get tasks with pre-populated cache
self.cached_tasks = fetch_all_tasks_with_due_dates(
self.jira_url, self.bearer_token, start_date, end_date, max_results, self.story_cache
)
results['tasks'] = self.cached_tasks or []
return results
```
#### 3.2 Update Report Transformation
**File**: `src/reports/transform_report_data.py`
```python
def _transform_task_section(report_data):
"""
Enhanced version that includes storypoints
"""
transformed_data = []
for task in report_data:
transformed_data.append([
task['issue'],
task['start'],
task['resolved'],
task['due_date'],
task['labels'],
task['responsible'],
task['parent_story'], # New field
task['storypoints'] # New field
])
return transformed_data
```
#### 3.3 Update Spreadsheet Formatter
**File**: `src/formatting/spreadsheet_formatter.py`
```python
def format_task_sheet(reports_data):
"""
Formats data for 'Tasks' sheet as a single long vertical list.
Returns: List of lists for sheet data
"""
headers = ["Task", "Start", "Resolved", "Due Date", "Labels", "Responsible", "Parent Story", "Story Points"]
return _format_single_list_sheet_global_data(reports_data['global_tasks'], headers)
```
## Implementation Details
### Parent Story Extraction Logic
```python
def _extract_parent_story_key(task_data: Dict) -> Optional[str]:
"""
Extracts parent story key from task issuelinks
Args:
task_data: Task data from JIRA API
Returns:
Parent story key or None if not found
"""
issuelinks = task_data.get('fields', {}).get('issuelinks', [])
for link in issuelinks:
link_type = link.get('type', {})
if link_type.get('inward') == 'Parent task':
inward_issue = link.get('inwardIssue')
if inward_issue:
return inward_issue.get('key')
return None
```
### Caching Strategy
Simple in-memory cache without expiration for maximum performance:
```python
def _get_or_fetch_storypoints(
story_key: str,
jira_url: str,
bearer_token: str,
story_cache: StoryCache
) -> Optional[int]:
"""
Gets storypoints from cache or fetches from API
Args:
story_key: Parent story key
jira_url: URL Jira API
bearer_token: Bearer токен для аутентификации
story_cache: Story cache instance
Returns:
Storypoints value or None if not found
"""
# Check cache first
cached_points = story_cache.get(story_key)
if cached_points is not None:
return cached_points
# Fetch from API
story_data = _fetch_parent_story_data(jira_url, bearer_token, story_key)
if story_data:
storypoints = story_data.get('fields', {}).get('customfield_10003')
story_cache.set(story_key, storypoints)
return storypoints
return None
```
**Cache Design:**
- Simple dictionary-based storage: `{story_key: storypoints}`
- No expiration policy (cache lasts for the duration of report generation)
- Automatic clearing when GlobalDataManager is reset
- Minimal memory footprint as only storypoints are stored
## Data Flow Diagram
```mermaid
sequenceDiagram
participant Client
participant ReportGen as Report Generator
participant GlobalDM as GlobalDataManager
participant JiraAPI as JIRA API
participant StoryCache as Story Cache
Client->>ReportGen: Generate Report
ReportGen->>GlobalDM: fetch_global_data
Note over GlobalDM: Phase 1: Fetch Stories and Pre-populate Cache
GlobalDM->>JiraAPI: fetch_all_stories_with_due_dates
JiraAPI-->>GlobalDM: Stories List
GlobalDM->>StoryCache: Pre-populate with all story storypoints
Note over GlobalDM: Phase 2: Fetch Tasks with Optimized Cache
GlobalDM->>JiraAPI: fetch_all_tasks_with_due_dates
JiraAPI-->>GlobalDM: Task List
loop For Each Task
GlobalDM->>GlobalDM: Extract Parent Story Key
alt Parent Story Found
GlobalDM->>StoryCache: get(story_key)
alt Cache Hit (Optimized)
StoryCache-->>GlobalDM: Cached Storypoints
else Cache Miss (Rare)
GlobalDM->>JiraAPI: _fetch_parent_story_data
JiraAPI-->>GlobalDM: Story Data
GlobalDM->>StoryCache: set(story_key, storypoints)
end
GlobalDM->>GlobalDM: Merge Task + Story Data
end
end
GlobalDM-->>ReportGen: Enhanced Task Data
ReportGen-->>Client: Report with Storypoints
```
## Error Handling
### API Error Handling
- Handle cases where parent story is not accessible
- Handle cases where story doesn't exist
- Implement retry logic for failed API calls
- Log errors appropriately
### Cache Error Handling
- Handle cache initialization failures
- Implement cache size limits
- Handle cache corruption scenarios
## Performance Considerations
### Caching Benefits
- Reduces API calls when multiple tasks share the same parent story
- Improves response time for large task sets
- Reduces load on JIRA API
- **Cache pre-population optimization**: Stories are fetched before tasks, so most parent story data is already cached when processing tasks
### Cache Pre-population Strategy
The implementation includes a key optimization: the cache is pre-populated with all stories and their storypoints before processing tasks. This provides significant benefits:
1. **Higher cache hit rate**: Since stories are fetched first, most parent stories referenced by tasks will already be in cache
2. **Reduced API calls**: Minimizes additional API requests during task processing
3. **Faster processing**: Tasks can be processed more efficiently with cached data
### Memory Management
- Simple in-memory cache without expiration
- Cache cleared when GlobalDataManager is reset
- Minimal memory footprint as only storypoints are stored
- Cache is populated once per report generation cycle
## Implementation Steps
1. Create StoryCache class in `src/api/story_cache.py`
2. Create parent story fetcher function in `src/api/client_jira.py`
3. Modify `fetch_all_tasks_with_due_dates` to include parent story data
4. Update `format_task_data` to include parent story and storypoints
5. Update `_transform_task_section` to include new fields
6. Update `format_task_sheet` to include new columns
7. Update GlobalDataManager to use story cache with pre-population
## Testing Strategy
The service runs in Docker Compose and can be tested by sending requests to the running service.
### Test Procedure
1. Start the service with `docker compose up`
2. Send a test request to generate a report:
```bash
curl -X POST "http://localhost:8055/create_report_in_spreadsheet" \
-H "Content-Type: application/json" \
-d '{
"table_id": "1HRhWYzVql1cTv0mfGPd1B0qMXA5yD0xchirlfQJJjEo",
"secret": "BRgln-IUjja-1kH6B-XONN9"
}'
```
3. If the report generates successfully without errors, the implementation is working correctly
4. The actual validation of the table data and storypoints will be performed manually
@@ -0,0 +1,7 @@
В данном проекте, в методе, который запирает все таски глобально fetch_all_tasks_with_due_dates() в client_jira.py необходимо добавить новый функционал. К каждой таске необходимо ещё получать дополнительную информацию – родительскую стори и сторипоинты родительской стори. Брать родительскую стори данной таски, из данной стори брать стори-поинты (stripoints), которые там есть, и добавлять потом к таске. При этом важно, поскольку тасок много, и некоторые из них принадлежат одной стори, то необходимо сделать кэширование. То есть мы берём, загружаем таску, лезем проверять её родительскую стори, забираем эти данные и кэшируем стори, чтобы в следующей, когда мы берём следующую таску, скорее всего, она попадёт в эту же стори. Тогда мы сначала проверяем кэш. В кеше достаточно хранить только словарь где ключи стори, а значение – сторипоинты. Если в кэше нет, тогда уже делаем запрос в жиру на получение родиельской стори и её сторипоинтов. То есть это нужно будет делать для каждой таски. Спроектируй архитектуру данных изменений и сохрани план работ в папке docs. Там же можно найти примеры планов от другого проекта. Необходимо аккуратно спроектировать данные изменения и аккуратно имплементировать во все места, где это может использоваться по флоу проекта. А ну и да, нужно будет в итоге в отчете добавить новое поле storypoints, по аналогии с отчетом, где собираются стори.
Информация о родительской таске находится в `fields.issuelinks`. Там список линков для данной таски. Нам нужна та у которой `type.inward == "Parent task"`. Там уже нужно найти "key" и это будет ключ на родительскую задачу. Забрать информацию по рдительской задаче и там найти сторипоинты. В проекте уже реализован сбор сторипоинтов. Если не указано, то поле пустое
Пример полной выдачи информации по таски от жиры в `docs\WPCMU-9082.json`
ВАЖНО! Всегда думай и отвечай на английском!