close_and_export_taiga.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. #!/usr/bin/env python
  2. #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
  3. import os
  4. import requests
  5. import json
  6. import sys
  7. from dotenv import load_dotenv
  8. import urllib3
  9. if hasattr(sys.stdout, 'reconfigure'):
  10. sys.stdout.reconfigure(encoding='utf-8')
  11. if hasattr(sys.stderr, 'reconfigure'):
  12. sys.stderr.reconfigure(encoding='utf-8')
  13. # Load local environment configuration
  14. load_dotenv()
  15. TAIGA_URL = os.getenv("TAIGA_URL")
  16. if TAIGA_URL:
  17. TAIGA_API_URL = f"{TAIGA_URL.rstrip('/')}/api/v1"
  18. TAIGA_USERNAME = os.getenv("TAIGA_USER", "FrancoisLange")
  19. TAIGA_PASSWORD = os.getenv("TAIGA_PASS")
  20. PROJECT_ID = os.getenv("TAIGA_PROJECT_ID", "21")
  21. else:
  22. TAIGA_API_URL = os.getenv("TAIGA_API_URL", "https://api.taiga.io/api/v1")
  23. TAIGA_USERNAME = os.getenv("TAIGA_USERNAME")
  24. TAIGA_PASSWORD = os.getenv("TAIGA_PASSWORD")
  25. PROJECT_ID = os.getenv("TAIGA_PROJECT_ID")
  26. urllib3.disable_warnings()
  27. session = requests.Session()
  28. session.verify = False
  29. def authenticate_taiga():
  30. print("[INFO] Authenticating with Taiga...")
  31. payload = {
  32. "type": "normal",
  33. "username": TAIGA_USERNAME,
  34. "password": TAIGA_PASSWORD
  35. }
  36. response = session.post(f"{TAIGA_API_URL}/auth", json=payload)
  37. response.raise_for_status()
  38. return response.json().get("auth_token")
  39. def get_headers(token):
  40. return {
  41. "Authorization": f"Bearer {token}",
  42. "Content-Type": "application/json"
  43. }
  44. def main():
  45. if not TAIGA_USERNAME or not TAIGA_PASSWORD or not PROJECT_ID:
  46. print("[ERROR] Missing Taiga credentials in .env")
  47. sys.exit(1)
  48. token = authenticate_taiga()
  49. headers = get_headers(token)
  50. # 1. Retrieve all User Stories to locate the target one
  51. print("[INFO] Fetching user stories...")
  52. r = session.get(f"{TAIGA_API_URL}/userstories?project={PROJECT_ID}", headers=headers)
  53. r.raise_for_status()
  54. user_stories = r.json()
  55. target_us_id = None
  56. target_subject = "As a team, we need enhanced communication and documentation."
  57. for us in user_stories:
  58. if us["subject"] == target_subject:
  59. target_us_id = us["id"]
  60. break
  61. if not target_us_id and user_stories:
  62. # Fallback to the first story if the target one isn't found
  63. target_us_id = user_stories[0]["id"]
  64. print(f"[WARNING] Target story not found. Using fallback story ID: {target_us_id}")
  65. elif not target_us_id:
  66. print("[ERROR] No user stories found in the project.")
  67. sys.exit(1)
  68. else:
  69. print(f"[INFO] Found target user story ID: {target_us_id}")
  70. # 2. Create the documentation task if it doesn't exist yet
  71. task_subject = "Create skill directory setup and configuration guide"
  72. # Check if task already exists
  73. print("[INFO] Checking if documentation task already exists...")
  74. r = session.get(f"{TAIGA_API_URL}/tasks?project={PROJECT_ID}", headers=headers)
  75. r.raise_for_status()
  76. tasks = r.json()
  77. task_exists = False
  78. for t in tasks:
  79. if t["subject"] == task_subject:
  80. task_exists = True
  81. break
  82. if not task_exists:
  83. print(f"[INFO] Creating task: '{task_subject}'...")
  84. task_payload = {
  85. "subject": task_subject,
  86. "project": int(PROJECT_ID),
  87. "user_story": target_us_id
  88. }
  89. r = session.post(f"{TAIGA_API_URL}/tasks", json=task_payload, headers=headers)
  90. r.raise_for_status()
  91. print("[SUCCESS] Task created successfully!")
  92. # Re-fetch tasks to include the new one
  93. r = session.get(f"{TAIGA_API_URL}/tasks?project={PROJECT_ID}", headers=headers)
  94. r.raise_for_status()
  95. tasks = r.json()
  96. else:
  97. print("[INFO] Documentation task already exists.")
  98. # 3. Fetch and close all tasks
  99. print(f"[INFO] Closing {len(tasks)} tasks...")
  100. for t in tasks:
  101. tid = t["id"]
  102. # Skip if already closed
  103. if t.get("status_extra_info", {}).get("is_closed", False) or t.get("is_closed", False):
  104. print(f" - Task already closed: {t['subject']}")
  105. continue
  106. print(f" - Closing task: {t['subject']} (ID: {tid}, Version: {t.get('version')})")
  107. # Try to find the closed status ID dynamically or use a standard close logic
  108. # In self-hosted Taiga, closed status might have a different ID, so let's check
  109. # status and set to is_closed.
  110. closed_status_id = None
  111. # We can fetch statuses to find one that is_closed
  112. try:
  113. statuses_resp = session.get(f"{TAIGA_API_URL}/task-statuses?project={PROJECT_ID}", headers=headers).json()
  114. closed_status_id = next((s["id"] for s in statuses_resp if s["is_closed"]), None)
  115. except Exception:
  116. pass
  117. if not closed_status_id:
  118. closed_status_id = 8901961 # Fallback
  119. r = session.patch(f"{TAIGA_API_URL}/tasks/{tid}", json={"status": closed_status_id, "version": t["version"]}, headers=headers)
  120. if not r.ok:
  121. print(f"[ERROR] Failed to close task {tid}: {r.text}")
  122. r.raise_for_status()
  123. # 4. Close all User Stories
  124. r = session.get(f"{TAIGA_API_URL}/userstories?project={PROJECT_ID}", headers=headers)
  125. r.raise_for_status()
  126. user_stories = r.json()
  127. print(f"[INFO] Closing {len(user_stories)} user stories...")
  128. for us in user_stories:
  129. usid = us["id"]
  130. if us.get("status_extra_info", {}).get("is_closed", False) or us.get("is_closed", False):
  131. print(f" - Story already closed: {us['subject']}")
  132. continue
  133. print(f" - Closing story: {us['subject']} (ID: {usid}, Version: {us.get('version')})")
  134. closed_us_status_id = None
  135. try:
  136. statuses_resp = session.get(f"{TAIGA_API_URL}/userstory-statuses?project={PROJECT_ID}", headers=headers).json()
  137. closed_us_status_id = next((s["id"] for s in statuses_resp if s["is_closed"]), None)
  138. except Exception:
  139. pass
  140. if not closed_us_status_id:
  141. closed_us_status_id = 10832456 # Fallback
  142. r = session.patch(f"{TAIGA_API_URL}/userstories/{usid}", json={"status": closed_us_status_id, "version": us["version"]}, headers=headers)
  143. if not r.ok:
  144. print(f"[ERROR] Failed to close story {usid}: {r.text}")
  145. r.raise_for_status()
  146. # 5. Close all Sprints/Milestones
  147. print("[INFO] Fetching Sprints...")
  148. r = session.get(f"{TAIGA_API_URL}/milestones?project={PROJECT_ID}", headers=headers)
  149. r.raise_for_status()
  150. milestones = r.json()
  151. print(f"[INFO] Closing {len(milestones)} milestones...")
  152. for m in milestones:
  153. mid = m["id"]
  154. if m["closed"]:
  155. print(f" - Milestone already closed: {m['name']}")
  156. continue
  157. print(f" - Closing milestone: {m['name']} (ID: {mid}, Version: {m.get('version')})")
  158. r = session.patch(f"{TAIGA_API_URL}/milestones/{mid}", json={"closed": True, "version": m.get("version")}, headers=headers)
  159. if not r.ok:
  160. print(f"[ERROR] Failed to close milestone {mid}: {r.text}")
  161. r.raise_for_status()
  162. # 6. Fetch final state and export to JSON
  163. print("[INFO] Fetching final project state for export...")
  164. r = session.get(f"{TAIGA_API_URL}/milestones?project={PROJECT_ID}", headers=headers)
  165. final_milestones = r.json()
  166. r = session.get(f"{TAIGA_API_URL}/userstories?project={PROJECT_ID}", headers=headers)
  167. final_stories = r.json()
  168. r = session.get(f"{TAIGA_API_URL}/tasks?project={PROJECT_ID}", headers=headers)
  169. final_tasks = r.json()
  170. export_data = {
  171. "project_id": PROJECT_ID,
  172. "sprints": [
  173. {
  174. "id": m["id"],
  175. "name": m["name"],
  176. "estimated_start": m["estimated_start"],
  177. "estimated_finish": m["estimated_finish"],
  178. "closed": m["closed"]
  179. } for m in final_milestones
  180. ],
  181. "user_stories": [
  182. {
  183. "id": us["id"],
  184. "subject": us["subject"],
  185. "status_id": us["status"],
  186. "is_closed": us.get("is_closed", False),
  187. "milestone": us["milestone"]
  188. } for us in final_stories
  189. ],
  190. "tasks": [
  191. {
  192. "id": t["id"],
  193. "subject": t["subject"],
  194. "status_id": t["status"],
  195. "is_closed": t.get("is_closed", False),
  196. "user_story": t["user_story"]
  197. } for t in final_tasks
  198. ]
  199. }
  200. base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  201. export_path = os.path.join(base_dir, "docs", "taiga_export.json")
  202. with open(export_path, "w", encoding="utf-8") as f:
  203. json.dump(export_data, f, indent=4, ensure_ascii=False)
  204. print(f"[SUCCESS] Saved export to: {export_path}")
  205. # Also save to the special taiga directory dump path if it exists
  206. taiga_dir = os.path.join(base_dir, "taiga")
  207. if os.path.exists(taiga_dir):
  208. special_path = os.path.join(taiga_dir, "local-food-ai-1-eab691c0-9c19-4dce-ac66-3b8fade77ef7.json")
  209. with open(special_path, "w", encoding="utf-8") as f:
  210. json.dump(export_data, f, indent=4, ensure_ascii=False)
  211. print(f"[SUCCESS] Saved export to: {special_path}")
  212. if __name__ == "__main__":
  213. main()