fix: исправление API-эндпоинтов GitFlic (GitLab-стиль -> GitFlic REST API)
This commit is contained in:
1 parent
08e551c8bc
commit
9dea59dd43
2 files changed
+90
-93
No files matched your search
@@ -4,6 +4,8 @@
|
||||
|
||||
gitflic:
|
||||
# Базовый URL GitFlic (облачный или self-hosted)
|
||||
# Для облачной версии: https://api.gitflic.ru
|
||||
# Для self-hosted: http://<host>:<port>/rest-api
|
||||
base_url: "https://gitflic.ru"
|
||||
# API-токен пользователя (read-only, scope: api, read_repository)
|
||||
api_token: "" # required
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user