191 lines
6.6 KiB
Python
191 lines
6.6 KiB
Python
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}"
|