reconcile_taiga_final_scrum.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import requests
  2. import urllib3
  3. import os
  4. import sys
  5. urllib3.disable_warnings()
  6. sys.stdout.reconfigure(encoding='utf-8')
  7. TAIGA_USER = os.environ.get('TAIGA_USER', 'FrancoisLange')
  8. TAIGA_PASS = os.environ.get('TAIGA_PASS', 'your_db_password_here')
  9. base_url = 'https://192.168.130.161/taiga/api/v1'
  10. def main():
  11. print("Connecting to Taiga...")
  12. auth_resp = requests.post(f'{base_url}/auth', json={'type': 'normal', 'username': TAIGA_USER, 'password': TAIGA_PASS}, verify=False)
  13. if auth_resp.status_code != 200:
  14. print("Auth failed!")
  15. return
  16. auth = auth_resp.json()
  17. headers = {
  18. 'Authorization': f'Bearer {auth["auth_token"]}',
  19. 'Content-Type': 'application/json',
  20. 'x-disable-pagination': 'true'
  21. }
  22. project_id = 21
  23. # 1. Fetch current Sprints (Milestones) to assign stories and tasks properly
  24. sprints = requests.get(f'{base_url}/milestones?project={project_id}', headers=headers, verify=False).json()
  25. # Find Sprint 7 (Production Hardening) or similar, let's use the first active sprint or default to sprint 13
  26. sprint_13 = next((s for s in sprints if "Sprint 13" in s['name']), None)
  27. if not sprint_13:
  28. sprint_13 = sprints[0]
  29. sprint_13_id = sprint_13['id']
  30. # 2. Define the 11 restricted user stories
  31. target_stories_def = {
  32. 1: {
  33. "subject": "US-1: Create an account and log in securely",
  34. "desc": "As a user, I want to create an account and log in securely, so that my personal clinical profiles and food lists are protected.",
  35. "match_keywords": ["account", "login", "auth"]
  36. },
  37. 2: {
  38. "subject": "US-2: Get complete nutritional value information on any food",
  39. "desc": "As a user, I want to get complete nutritional value information on any food (including macro nutrients, minerals, vitamins, amino acids etc.), so that I can audit my dietary intake in detail.",
  40. "match_keywords": ["nutritional info", "macro", "mineral", "vitamin", "ingest", "pandas", "dynamic rebuild", "database schema"]
  41. },
  42. 3: {
  43. "subject": "US-3: Get the full nutritional value overview for a given food combination",
  44. "desc": "As a user, I want to get the full nutritional value overview for a given food combination by entering their quantities, so that I can see the total impact of a recipe or meal.",
  45. "match_keywords": ["food combination", "quantities", "overview", "scale conversion"]
  46. },
  47. 4: {
  48. "subject": "US-4: Search for specific nutrient content and get a sortable list of all foods",
  49. "desc": "As a user, I want to search for specific nutrient content and get a sortable list of all foods that contain them, so that I can easily find foods that satisfy my micro/macro targets.",
  50. "match_keywords": ["search for nutrient", "sortable list"]
  51. },
  52. 5: {
  53. "subject": "US-5: Store food combinations in named and editable lists",
  54. "desc": "As a user, I want to store food combinations in named and editable lists, so that I can save my favorite meals for quick access.",
  55. "match_keywords": ["store food combinations", "editable list"]
  56. },
  57. 6: {
  58. "subject": "US-6: Get menu proposals based on nutritional value and health constraints",
  59. "desc": "As a user, I want to get menu proposals based on nutritional value targets and health/allergy constraints, so that I can automatically follow a safe, tailored diet.",
  60. "match_keywords": ["menu proposal", "allergy", "dietary preference", "clinical warning", "pdf", "profiler"]
  61. },
  62. 7: {
  63. "subject": "US-7: Freely chat about anything related to nutrition and get competent answers",
  64. "desc": "As a user, I want to freely chat with a competent clinical AI dietitian, so that I can ask any nutrition-related questions and get expert advice.",
  65. "match_keywords": ["chat", "nutrition", "dietitian"]
  66. },
  67. 8: {
  68. "subject": "US-8: Anonymous private web search tool (SearXNG) integration",
  69. "desc": "As a system operator, I want the AI to utilize a local private web search tool (SearXNG) for anonymous information retrieval when the local database is insufficient, so that the AI can answer external queries without cloud APIs.",
  70. "match_keywords": ["web search", "searxng", "anonymous"]
  71. },
  72. 9: {
  73. "subject": "US-9: 100% local data privacy (no user data leaves the server)",
  74. "desc": "As a privacy-focused user, I want all computations and data storage to remain strictly local on the server, so that absolutely no user data leaves the server boundary.",
  75. "match_keywords": ["privacy", "data leaves", "password rotation", "security"]
  76. },
  77. 10: {
  78. "subject": "US-10: Public Git repository with easy cloning support (LocalFoodAI_lanfr144)",
  79. "desc": "As a developer/student, I want the project to reside in a public, real-time updated repo named LocalFoodAI_lanfr144 with easy cloning support and my teacher as a collaborator, so that anyone can deploy the app effortlessly.",
  80. "match_keywords": ["git", "cloning", "repo", "collaborator", "runbook", "rituals"]
  81. },
  82. 11: {
  83. "subject": "US-11: Local hardware boundary containment on Ubuntu 24.04 VM",
  84. "desc": "As a system architect, I want the application to run entirely on the provided Ubuntu VM (8 vCPUs, 30 GB RAM) utilizing optimized local LLMs and databases, so that we maximize performance without exceeding physical limits.",
  85. "match_keywords": ["ubuntu", "hardware", "cpu", "ram", "ollama", "qwen", "mistral", "zabbix", "snmp", "telemetry", "log rotation", "dr testing", "airflow", "mysql", "performance", "cartesian"]
  86. }
  87. }
  88. # 3. Fetch all current user stories to map or create
  89. stories = requests.get(f'{base_url}/userstories?project={project_id}', headers=headers, verify=False).json()
  90. print(f"Fetched {len(stories)} current user stories.")
  91. # We will rename the 11 closest existing stories to match our subjects, and delete the rest
  92. mapped_stories = {} # key: 1..11, value: story object or created story object
  93. # Map the existing stories that already perfectly correspond to our points
  94. keyword_to_target = {
  95. "User Account Creation": 1,
  96. "View Complete Nutritional Info": 2,
  97. "Combined Nutritional Value Overview": 3,
  98. "Search for Nutrients": 4,
  99. "Store and Edit Food": 5,
  100. "AI Menu Proposals": 6,
  101. "Chat About Nutrition": 7,
  102. "Anonymous Web Search": 8,
  103. "Local Data Privacy": 9,
  104. "Public Git Repo": 10,
  105. "Lightweight Local AI": 11
  106. }
  107. for s in stories:
  108. subj = s['subject']
  109. for kw, target_id in keyword_to_target.items():
  110. if kw in subj:
  111. mapped_stories[target_id] = s
  112. print(f"Mapped existing US Ref {s['ref']} '{subj}' to Target US-{target_id}")
  113. break
  114. # For any target story not mapped, we'll create a new story
  115. for target_id, t_def in target_stories_def.items():
  116. if target_id not in mapped_stories:
  117. # Let's check if there are other stories we can hijack, or create new
  118. print(f"Target US-{target_id} not mapped. Creating new user story...")
  119. payload = {
  120. "project": project_id,
  121. "milestone": sprint_13_id,
  122. "subject": t_def["subject"],
  123. "description": t_def["desc"],
  124. "status": 125 # Done status
  125. }
  126. res = requests.post(f'{base_url}/userstories', json=payload, headers=headers, verify=False)
  127. if res.status_code == 201:
  128. mapped_stories[target_id] = res.json()
  129. print(f"Successfully created US-{target_id}: '{t_def['subject']}' (ID: {mapped_stories[target_id]['id']})")
  130. else:
  131. print(f"Failed to create US-{target_id}: {res.text}")
  132. else:
  133. # Update the subject and description of mapped existing story
  134. s = mapped_stories[target_id]
  135. t_def = target_stories_def[target_id]
  136. print(f"Updating existing US Ref {s['ref']} to '{t_def['subject']}'...")
  137. payload = {
  138. "subject": t_def["subject"],
  139. "description": t_def["desc"],
  140. "version": s["version"],
  141. "status": 125 # Done status
  142. }
  143. res = requests.patch(f'{base_url}/userstories/{s["id"]}', json=payload, headers=headers, verify=False)
  144. if res.status_code == 200:
  145. mapped_stories[target_id] = res.json()
  146. print(f"Successfully updated US-{target_id}")
  147. else:
  148. print(f"Failed to update US-{target_id}: {res.text}")
  149. # Set of target story IDs in Taiga
  150. target_story_ids = {s['id'] for s in mapped_stories.values()}
  151. print(f"Target User Story IDs in Taiga: {target_story_ids}")
  152. # 4. Fetch all tasks to migrate
  153. tasks = requests.get(f'{base_url}/tasks?project={project_id}', headers=headers, verify=False).json()
  154. print(f"Fetched {len(tasks)} tasks.")
  155. # 5. Map tasks to target story IDs
  156. for t in tasks:
  157. current_us_id = t.get('user_story')
  158. current_us_subject = t.get('user_story_extra_info', {}).get('subject', '') if t.get('user_story_extra_info') else ''
  159. # If task is already under one of the 11 target stories, no need to change
  160. if current_us_id in target_story_ids:
  161. # Let's ensure it is in closed status
  162. if t['status'] != 104:
  163. patch_payload = {"status": 104, "version": t["version"]}
  164. requests.patch(f'{base_url}/tasks/{t["id"]}', json=patch_payload, headers=headers, verify=False)
  165. continue
  166. # Determine which target story this task belongs to
  167. task_subj = t['subject'].lower()
  168. task_desc = (t.get('description') or '').lower()
  169. combined_text = task_subj + " " + task_desc + " " + current_us_subject.lower()
  170. assigned_target_id = 11 # Default fallback to Infrastructure
  171. # Check matching keywords
  172. matched = False
  173. for target_id, t_def in target_stories_def.items():
  174. for kw in t_def["match_keywords"]:
  175. if kw in combined_text:
  176. assigned_target_id = target_id
  177. matched = True
  178. break
  179. if matched:
  180. break
  181. target_us = mapped_stories[assigned_target_id]
  182. print(f"Migrating Task TG-{t['ref']} '{t['subject']}' to US-{assigned_target_id} '{target_us['subject']}'...")
  183. # Patch task to reassign user story and ensure closed status
  184. patch_payload = {
  185. "user_story": target_us['id'],
  186. "status": 104, # Closed status
  187. "version": t["version"]
  188. }
  189. res = requests.patch(f'{base_url}/tasks/{t["id"]}', json=patch_payload, headers=headers, verify=False)
  190. if res.status_code == 200:
  191. print(f" Successfully migrated Task TG-{t['ref']}")
  192. else:
  193. print(f" Failed to migrate Task TG-{t['ref']}: {res.text}")
  194. # 6. Delete all other purely technical user stories
  195. all_stories = requests.get(f'{base_url}/userstories?project={project_id}', headers=headers, verify=False).json()
  196. deleted_count = 0
  197. for s in all_stories:
  198. if s['id'] not in target_story_ids:
  199. print(f"Deleting technical user story Ref {s['ref']} '{s['subject']}'...")
  200. res = requests.delete(f'{base_url}/userstories/{s["id"]}', headers=headers, verify=False)
  201. if res.status_code in [200, 204]:
  202. print(f" Successfully deleted US Ref {s['ref']}")
  203. deleted_count += 1
  204. else:
  205. print(f" Failed to delete US Ref {s['ref']}: {res.text}")
  206. print(f"Reconciliation completed successfully! Deleted {deleted_count} technical stories.")
  207. if __name__ == '__main__':
  208. main()