fix: исправление API-эндпоинтов GitFlic (GitLab-стиль -> GitFlic REST API)

This commit is contained in:
redlineo committed 2026-09-16 17:43:28 +03:00
1 parent 08e551c8bc
commit 9dea59dd43
2 files changed
+90 -93

No files matched your search

+88 -93
View File
@@ -13,7 +13,7 @@ import time
from copy import deepcopy
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple
import requests
import yaml
@@ -100,21 +100,21 @@ def save_state(state: dict, path: str) -> None:
logger.error("Ошибка сохранения состояния в %s: %s", path, exc)
def make_release_key(project_id: int, release_id: int,
def make_release_key(project_id: str, release_id: str,
tag: str, version: str) -> str:
"""Сформировать ключ для идентификации релиза."""
return f"{project_id}:{release_id}:{tag}:{version}"
def is_release_sent(state: dict, project_id: int, release_id: int,
def is_release_sent(state: dict, project_id: str, release_id: str,
tag: str, version: str) -> bool:
"""Проверить, был ли релиз уже отправлен."""
key = make_release_key(project_id, release_id, tag, version)
return key in state.get("sent_releases", {})
def mark_release_sent(state: dict, project_id: int, project_name: str,
release_id: int, tag: str, version: str,
def mark_release_sent(state: dict, project_id: str, project_name: str,
release_id: str, tag: str, version: str,
files: List[Dict[str, Any]]) -> None:
"""Пометить релиз как отправленный."""
key = make_release_key(project_id, release_id, tag, version)
@@ -153,7 +153,7 @@ class GitFlicClient:
self._session = requests.Session()
self._session.headers.update({
"Authorization": f"Bearer {self.api_token}",
"Authorization": f"token {self.api_token}",
"Accept": "application/json",
"User-Agent": "Sandbox-GitFlic-PoC/1.0",
})
@@ -176,43 +176,48 @@ class GitFlicClient:
logger.error("Ошибка запроса %s %s: %s", method, url, exc)
raise
def _paginated_get(self, path: str, params: Optional[dict] = None) -> List[dict]:
"""Получить все страницы пагинированного GET-запроса."""
def _paginated_get(self, path: str, params: Optional[dict] = None,
list_key: Optional[str] = None) -> List[dict]:
"""
Получить все страницы пагинированного GET-запроса.
GitFlic использует: size (не per_page), page начиная с 0,
и ответ в формате {"_embedded": {"<listKey>": [...]}, "page": {...}}.
"""
if params is None:
params = {}
params.setdefault("per_page", self.per_page)
params.setdefault("size", self.per_page)
page = 1
page = 0
all_items: List[dict] = []
while True:
params["page"] = page
resp = self._request("GET", path, params=params)
items = resp.json()
if not isinstance(items, list):
all_items = items
break
data = resp.json()
if list_key:
items = data.get("_embedded", {}).get(list_key, [])
else:
embedded = data.get("_embedded", {})
items = next(iter(embedded.values())) if embedded else []
all_items.extend(items)
total_pages = resp.headers.get("X-Total-Pages")
if total_pages is not None:
if page >= int(total_pages):
break
else:
if not items:
break
page_info = data.get("page", {})
total_pages = page_info.get("totalPages", 0)
if page >= total_pages - 1:
break
page += 1
return all_items
def get_projects(self) -> List[dict]:
"""GET /api/v1/projects."""
return self._paginated_get("/api/v1/projects", {"membership": True})
def get_my_projects(self) -> List[dict]:
"""GET /project/my — собственные проекты пользователя."""
return self._paginated_get("/project/my", list_key="projectList")
def get_company_projects(self, company_id: int) -> List[dict]:
"""GET /api/v1/companies/{id}/projects."""
return self._paginated_get(f"/api/v1/companies/{company_id}/projects")
def get_shared_projects(self) -> List[dict]:
"""GET /project/shared — проекты с участием пользователя."""
return self._paginated_get("/project/shared", list_key="projectList")
def resolve_projects(self, cfg: dict) -> List[dict]:
"""Определить список проектов для сканирования."""
@@ -234,7 +239,7 @@ class GitFlicClient:
if proj:
projects.append(proj)
else:
logger.warning("Проект не найден по ID: %d", pid)
logger.warning("Проект не найден по ID: %s", pid)
return projects
if source == "company":
@@ -251,45 +256,47 @@ class GitFlicClient:
return []
return self.get_team_projects(tid)
return self.get_projects()
return self.get_shared_projects()
def get_project(self, project_id: int) -> Optional[dict]:
def get_project(self, project_id: str) -> Optional[dict]:
"""GET /project/{projectUuid}."""
try:
resp = self._request("GET", f"/api/v1/projects/{project_id}")
resp = self._request("GET", f"/project/{project_id}")
return resp.json()
except requests.RequestException:
return None
def get_project_by_path(self, path: str) -> Optional[dict]:
"""
GET /project/{ownerAlias}/{projectAlias}.
path — строка вида "owner/project-name".
"""
try:
resp = self._request("GET", f"/api/v1/projects/{path}")
resp = self._request("GET", f"/project/{path}")
return resp.json()
except requests.RequestException:
return None
def get_releases(self, project_id: int) -> List[dict]:
"""GET /api/v1/projects/{id}/releases."""
def get_releases(self, owner_alias: str, project_alias: str) -> List[dict]:
"""GET /project/{ownerAlias}/{projectAlias}/release."""
return self._paginated_get(
f"/api/v1/projects/{project_id}/releases",
{"per_page": self.per_page},
f"/project/{owner_alias}/{project_alias}/release",
list_key="releaseTagModelList",
)
def get_release_assets(self, project_id: int, release_id: int) -> List[dict]:
"""Получить assets релиза."""
try:
resp = self._request(
"GET",
f"/api/v1/projects/{project_id}/releases/{release_id}/assets",
)
items = resp.json()
if isinstance(items, list):
return items
if isinstance(items, dict):
return items.get("assets", items.get("links", []))
return []
except requests.RequestException:
logger.debug("Assets не получены: %s/%s", project_id, release_id)
return []
def get_company_projects(self, company_id: str) -> List[dict]:
"""GET /company/{id}/project."""
return self._paginated_get(
f"/company/{company_id}/project",
list_key="projectList",
)
def get_team_projects(self, team_id: str) -> List[dict]:
"""GET /team/{id}/project."""
return self._paginated_get(
f"/team/{team_id}/project",
list_key="projectList",
)
def download_file(self, url: str, local_path: Path) -> Path:
"""Скачать файл по ссылке."""
@@ -298,7 +305,7 @@ class GitFlicClient:
headers = {}
if url.startswith(self.base_url):
headers["Authorization"] = f"Bearer {self.api_token}"
headers["Authorization"] = f"token {self.api_token}"
logger.info("Скачивание: %s -> %s", url, local_path)
resp = self._session.get(url, headers=headers, stream=True,
@@ -314,19 +321,6 @@ class GitFlicClient:
local_path, local_path.stat().st_size)
return local_path
def get_release_source_archive_url(self, project_id: int,
tag_name: str) -> str:
"""URL архива исходного кода по тегу релиза."""
return (f"{self.base_url}/api/v1/projects/{project_id}"
f"/repository/archive.zip?ref={tag_name}")
def get_release_asset_download_url(self, project_id: int,
release_id: int,
asset_filename: str) -> str:
"""URL для скачивания прикреплённого файла (asset)."""
return (f"{self.base_url}/api/v1/projects/{project_id}"
f"/releases/{release_id}/downloads/{asset_filename}")
# ─────────────────────────────────────────────────────────
# Клиент PT Sandbox
@@ -422,7 +416,7 @@ def scan_new_releases(gf: GitFlicClient, sb: PTSandboxClient,
1. Получить проекты
2. Для каждого проекта релизы
3. Отфильтровать отправленные
4. Скачать файлы (архив + assets)
4. Скачать файлы (attachmentFiles из релиза)
5. Отправить в Sandbox
6. Обновить состояние
"""
@@ -437,25 +431,32 @@ def scan_new_releases(gf: GitFlicClient, sb: PTSandboxClient,
for project in projects:
pid = project.get("id")
pname = project.get("path_with_namespace") or project.get("name", str(pid))
logger.info("Обработка проекта: %s (id=%s)", pname, pid)
owner_alias = project.get("owner", {}).get("alias", "")
project_alias = project.get("alias", "")
pname = f"{owner_alias}/{project_alias}"
logger.info("Обработка проекта: %s (uuid=%s)", pname, pid)
if not owner_alias or not project_alias:
logger.warning("Пропускаем проект %s: нет owner/project alias", pid)
continue
try:
releases = gf.get_releases(pid)
releases = gf.get_releases(owner_alias, project_alias)
except requests.RequestException:
logger.error("Пропускаем проект %s - ошибка получения релизов", pname)
continue
if not releases:
logger.info("Нет релизов в проекте %s", pname)
continue
releases.sort(key=lambda r: r.get("created_at", ""), reverse=True)
releases.sort(key=lambda r: r.get("createdAt", ""), reverse=True)
if scan_mode == "latest":
rel = releases[0]
rel_id = rel.get("id")
rel_tag = rel.get("tag_name", "")
rel_version = rel.get("name", "")
rel_tag = rel.get("tagName", "")
rel_version = rel.get("title", "")
if is_release_sent(state, pid, rel_id, rel_tag, rel_version):
logger.info("Последний релиз уже отправлен. Пропускаем.")
@@ -465,7 +466,7 @@ def scan_new_releases(gf: GitFlicClient, sb: PTSandboxClient,
releases_to_process = [
r for r in releases
if not is_release_sent(state, pid, r.get("id"),
r.get("tag_name", ""), r.get("name", ""))
r.get("tagName", ""), r.get("title", ""))
]
if not releases_to_process:
logger.info("Нет новых релизов в проекте %s", pname)
@@ -473,32 +474,26 @@ def scan_new_releases(gf: GitFlicClient, sb: PTSandboxClient,
for rel in releases_to_process:
rel_id = rel.get("id")
rel_tag = rel.get("tag_name", "")
rel_version = rel.get("name", "")
rel_tag = rel.get("tagName", "")
rel_version = rel.get("title", "")
rel_key = make_release_key(pid, rel_id, rel_tag, rel_version)
logger.info("Новый релиз: %s (tag=%s, version=%s)",
logger.info("Новый релиз: %s (tag=%s, title=%s)",
rel_key, rel_tag, rel_version)
# Собираем файлы для скачивания
# Собираем файлы для скачивания (attachmentFiles из релиза)
files_to_download: List[Dict[str, Any]] = []
local_files: List[Dict[str, Any]] = []
# 1. Архив исходного кода по тегу
source_url = gf.get_release_source_archive_url(pid, rel_tag)
sf = f"source_{pname.replace('/', '_')}_{rel_tag}.zip"
files_to_download.append({"type": "source_archive", "url": source_url, "filename": sf})
assets = rel.get("attachmentFiles", [])
for asset in assets:
aname = asset.get("name", "")
aurl = asset.get("link", "")
if not aname or not aurl:
continue
files_to_download.append({"type": "asset", "url": aurl, "filename": aname})
# 2. Assets (прикреплённые файлы)
try:
assets = gf.get_release_assets(pid, rel_id)
for asset in assets:
aname = asset.get("name") or asset.get("filename", "")
if not aname:
continue
aurl = gf.get_release_asset_download_url(pid, rel_id, aname)
files_to_download.append({"type": "asset", "url": aurl, "filename": aname})
except requests.RequestException:
logger.debug("Assets для %s не получены", rel_key)
if not files_to_download:
logger.info("Нет файлов для скачивания в релизе %s", rel_key)
# Скачиваем все файлы
for fi in files_to_download: