Taiga integration

This commit is contained in:
2026-06-09 12:47:13 +03:00
parent c8599b3d13
commit 1f83dcb574
30 changed files with 1543 additions and 115 deletions
+190
View File
@@ -0,0 +1,190 @@
from typing import Any
import httpx
from app.config import get_settings
class TaigaClient:
def __init__(self) -> None:
settings = get_settings()
self.base_url = settings.taiga_base_url.rstrip("/")
self.public_url = settings.taiga_public_url.rstrip("/")
self.username = settings.taiga_username
self.password = settings.taiga_password
self._token: str | None = None
def _client(self) -> httpx.Client:
return httpx.Client(base_url=self.base_url, timeout=30.0)
def auth(self) -> str:
if self._token:
return self._token
with self._client() as client:
response = client.post(
"/api/v1/auth",
json={
"type": "normal",
"username": self.username,
"password": self.password,
},
)
response.raise_for_status()
self._token = response.json()["auth_token"]
return self._token
def _headers(self) -> dict[str, str]:
return {
"Authorization": f"Bearer {self.auth()}",
"Content-Type": "application/json",
}
def list_projects(self) -> list[dict[str, Any]]:
with self._client() as client:
response = client.get("/api/v1/projects", headers=self._headers())
response.raise_for_status()
return response.json()
def list_open_userstories(self, project_id: int, limit: int = 8) -> list[dict[str, Any]]:
with self._client() as client:
response = client.get(
"/api/v1/userstories",
params={"project": project_id},
headers=self._headers(),
)
response.raise_for_status()
open_stories = [s for s in response.json() if not s.get("is_closed")]
return open_stories[:limit]
def list_open_tasks(self, project_id: int, limit: int = 8) -> list[dict[str, Any]]:
with self._client() as client:
response = client.get(
"/api/v1/tasks",
params={"project": project_id},
headers=self._headers(),
)
response.raise_for_status()
open_tasks = [t for t in response.json() if not t.get("is_closed")]
return open_tasks[:limit]
def get_closed_status_id(self, project_id: int, *, for_task: bool = False) -> int | None:
endpoint = "/api/v1/task-statuses" if for_task else "/api/v1/userstory-statuses"
with self._client() as client:
response = client.get(
endpoint,
params={"project": project_id},
headers=self._headers(),
)
response.raise_for_status()
items = response.json()
for status in items:
if status.get("is_closed") or status.get("name", "").lower() in (
"done",
"closed",
"завершено",
"закрыто",
):
return status["id"]
return items[-1]["id"] if items else None
def create_userstory(
self,
project_id: int,
subject: str,
description: str,
tags: list[str] | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"project": project_id,
"subject": subject[:500],
"description": description,
}
if tags:
payload["tags"] = tags
with self._client() as client:
response = client.post(
"/api/v1/userstories",
headers=self._headers(),
json=payload,
)
response.raise_for_status()
return response.json()
def create_task(
self,
project_id: int,
user_story_id: int,
subject: str,
description: str = "",
) -> dict[str, Any]:
with self._client() as client:
response = client.post(
"/api/v1/tasks",
headers=self._headers(),
json={
"project": project_id,
"user_story": user_story_id,
"subject": subject[:500],
"description": description,
},
)
response.raise_for_status()
return response.json()
def close_userstory(self, story_id: int, project_id: int) -> dict[str, Any]:
status_id = self.get_closed_status_id(project_id, for_task=False)
payload: dict[str, Any] = {"version": self._get_version("userstories", story_id)}
if status_id:
payload["status"] = status_id
else:
payload["is_closed"] = True
with self._client() as client:
response = client.patch(
f"/api/v1/userstories/{story_id}",
headers=self._headers(),
json=payload,
)
response.raise_for_status()
return response.json()
def close_task(self, task_id: int, project_id: int) -> dict[str, Any]:
status_id = self.get_closed_status_id(project_id, for_task=True)
payload: dict[str, Any] = {"version": self._get_version("tasks", task_id)}
if status_id:
payload["status"] = status_id
else:
payload["is_closed"] = True
with self._client() as client:
response = client.patch(
f"/api/v1/tasks/{task_id}",
headers=self._headers(),
json=payload,
)
response.raise_for_status()
return response.json()
def get_by_ref(
self, project_id: int, ref: int, *, kind: str = "userstory"
) -> dict[str, Any] | None:
endpoint = "/api/v1/userstories" if kind == "userstory" else "/api/v1/tasks"
with self._client() as client:
response = client.get(
endpoint,
params={"project": project_id, "ref": ref},
headers=self._headers(),
)
response.raise_for_status()
items = response.json()
return items[0] if items else None
def _get_version(self, resource: str, item_id: int) -> int:
with self._client() as client:
response = client.get(
f"/api/v1/{resource}/{item_id}",
headers=self._headers(),
)
response.raise_for_status()
return response.json().get("version", 1)
def story_url(self, project_id: int, ref: int) -> str:
return f"{self.public_url}/project/0/{project_id}/us/{ref}"