Taiga integration
This commit is contained in:
@@ -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}"
|
||||
Reference in New Issue
Block a user