fix: исправление API-эндпоинтов GitFlic (GitLab-стиль -> GitFlic REST API)
This commit is contained in:
1 parent
08e551c8bc
commit
9dea59dd43
2 files changed
+86
-89
No files matched your search
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
gitflic:
|
gitflic:
|
||||||
# Базовый URL GitFlic (облачный или self-hosted)
|
# Базовый URL GitFlic (облачный или self-hosted)
|
||||||
|
# Для облачной версии: https://api.gitflic.ru
|
||||||
|
# Для self-hosted: http://<host>:<port>/rest-api
|
||||||
base_url: "https://gitflic.ru"
|
base_url: "https://gitflic.ru"
|
||||||
# API-токен пользователя (read-only, scope: api, read_repository)
|
# API-токен пользователя (read-only, scope: api, read_repository)
|
||||||
api_token: "" # required
|
api_token: "" # required
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import time
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
@@ -100,21 +100,21 @@ def save_state(state: dict, path: str) -> None:
|
|||||||
logger.error("Ошибка сохранения состояния в %s: %s", path, exc)
|
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:
|
tag: str, version: str) -> str:
|
||||||
"""Сформировать ключ для идентификации релиза."""
|
"""Сформировать ключ для идентификации релиза."""
|
||||||
return f"{project_id}:{release_id}:{tag}:{version}"
|
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:
|
tag: str, version: str) -> bool:
|
||||||
"""Проверить, был ли релиз уже отправлен."""
|
"""Проверить, был ли релиз уже отправлен."""
|
||||||
key = make_release_key(project_id, release_id, tag, version)
|
key = make_release_key(project_id, release_id, tag, version)
|
||||||
return key in state.get("sent_releases", {})
|
return key in state.get("sent_releases", {})
|
||||||
|
|
||||||
|
|
||||||
def mark_release_sent(state: dict, project_id: int, project_name: str,
|
def mark_release_sent(state: dict, project_id: str, project_name: str,
|
||||||
release_id: int, tag: str, version: str,
|
release_id: str, tag: str, version: str,
|
||||||
files: List[Dict[str, Any]]) -> None:
|
files: List[Dict[str, Any]]) -> None:
|
||||||
"""Пометить релиз как отправленный."""
|
"""Пометить релиз как отправленный."""
|
||||||
key = make_release_key(project_id, release_id, tag, version)
|
key = make_release_key(project_id, release_id, tag, version)
|
||||||
@@ -153,7 +153,7 @@ class GitFlicClient:
|
|||||||
|
|
||||||
self._session = requests.Session()
|
self._session = requests.Session()
|
||||||
self._session.headers.update({
|
self._session.headers.update({
|
||||||
"Authorization": f"Bearer {self.api_token}",
|
"Authorization": f"token {self.api_token}",
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"User-Agent": "Sandbox-GitFlic-PoC/1.0",
|
"User-Agent": "Sandbox-GitFlic-PoC/1.0",
|
||||||
})
|
})
|
||||||
@@ -176,43 +176,48 @@ class GitFlicClient:
|
|||||||
logger.error("Ошибка запроса %s %s: %s", method, url, exc)
|
logger.error("Ошибка запроса %s %s: %s", method, url, exc)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _paginated_get(self, path: str, params: Optional[dict] = None) -> List[dict]:
|
def _paginated_get(self, path: str, params: Optional[dict] = None,
|
||||||
"""Получить все страницы пагинированного GET-запроса."""
|
list_key: Optional[str] = None) -> List[dict]:
|
||||||
|
"""
|
||||||
|
Получить все страницы пагинированного GET-запроса.
|
||||||
|
GitFlic использует: size (не per_page), page начиная с 0,
|
||||||
|
и ответ в формате {"_embedded": {"<listKey>": [...]}, "page": {...}}.
|
||||||
|
"""
|
||||||
if params is None:
|
if params is None:
|
||||||
params = {}
|
params = {}
|
||||||
params.setdefault("per_page", self.per_page)
|
params.setdefault("size", self.per_page)
|
||||||
|
|
||||||
page = 1
|
page = 0
|
||||||
all_items: List[dict] = []
|
all_items: List[dict] = []
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
params["page"] = page
|
params["page"] = page
|
||||||
resp = self._request("GET", path, params=params)
|
resp = self._request("GET", path, params=params)
|
||||||
items = resp.json()
|
data = resp.json()
|
||||||
if not isinstance(items, list):
|
|
||||||
all_items = items
|
if list_key:
|
||||||
break
|
items = data.get("_embedded", {}).get(list_key, [])
|
||||||
|
else:
|
||||||
|
embedded = data.get("_embedded", {})
|
||||||
|
items = next(iter(embedded.values())) if embedded else []
|
||||||
|
|
||||||
all_items.extend(items)
|
all_items.extend(items)
|
||||||
|
|
||||||
total_pages = resp.headers.get("X-Total-Pages")
|
page_info = data.get("page", {})
|
||||||
if total_pages is not None:
|
total_pages = page_info.get("totalPages", 0)
|
||||||
if page >= int(total_pages):
|
if page >= total_pages - 1:
|
||||||
break
|
|
||||||
else:
|
|
||||||
if not items:
|
|
||||||
break
|
break
|
||||||
page += 1
|
page += 1
|
||||||
|
|
||||||
return all_items
|
return all_items
|
||||||
|
|
||||||
def get_projects(self) -> List[dict]:
|
def get_my_projects(self) -> List[dict]:
|
||||||
"""GET /api/v1/projects."""
|
"""GET /project/my — собственные проекты пользователя."""
|
||||||
return self._paginated_get("/api/v1/projects", {"membership": True})
|
return self._paginated_get("/project/my", list_key="projectList")
|
||||||
|
|
||||||
def get_company_projects(self, company_id: int) -> List[dict]:
|
def get_shared_projects(self) -> List[dict]:
|
||||||
"""GET /api/v1/companies/{id}/projects."""
|
"""GET /project/shared — проекты с участием пользователя."""
|
||||||
return self._paginated_get(f"/api/v1/companies/{company_id}/projects")
|
return self._paginated_get("/project/shared", list_key="projectList")
|
||||||
|
|
||||||
def resolve_projects(self, cfg: dict) -> List[dict]:
|
def resolve_projects(self, cfg: dict) -> List[dict]:
|
||||||
"""Определить список проектов для сканирования."""
|
"""Определить список проектов для сканирования."""
|
||||||
@@ -234,7 +239,7 @@ class GitFlicClient:
|
|||||||
if proj:
|
if proj:
|
||||||
projects.append(proj)
|
projects.append(proj)
|
||||||
else:
|
else:
|
||||||
logger.warning("Проект не найден по ID: %d", pid)
|
logger.warning("Проект не найден по ID: %s", pid)
|
||||||
return projects
|
return projects
|
||||||
|
|
||||||
if source == "company":
|
if source == "company":
|
||||||
@@ -251,45 +256,47 @@ class GitFlicClient:
|
|||||||
return []
|
return []
|
||||||
return self.get_team_projects(tid)
|
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:
|
try:
|
||||||
resp = self._request("GET", f"/api/v1/projects/{project_id}")
|
resp = self._request("GET", f"/project/{project_id}")
|
||||||
return resp.json()
|
return resp.json()
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_project_by_path(self, path: str) -> Optional[dict]:
|
def get_project_by_path(self, path: str) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
GET /project/{ownerAlias}/{projectAlias}.
|
||||||
|
path — строка вида "owner/project-name".
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
resp = self._request("GET", f"/api/v1/projects/{path}")
|
resp = self._request("GET", f"/project/{path}")
|
||||||
return resp.json()
|
return resp.json()
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_releases(self, project_id: int) -> List[dict]:
|
def get_releases(self, owner_alias: str, project_alias: str) -> List[dict]:
|
||||||
"""GET /api/v1/projects/{id}/releases."""
|
"""GET /project/{ownerAlias}/{projectAlias}/release."""
|
||||||
return self._paginated_get(
|
return self._paginated_get(
|
||||||
f"/api/v1/projects/{project_id}/releases",
|
f"/project/{owner_alias}/{project_alias}/release",
|
||||||
{"per_page": self.per_page},
|
list_key="releaseTagModelList",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_release_assets(self, project_id: int, release_id: int) -> List[dict]:
|
def get_company_projects(self, company_id: str) -> List[dict]:
|
||||||
"""Получить assets релиза."""
|
"""GET /company/{id}/project."""
|
||||||
try:
|
return self._paginated_get(
|
||||||
resp = self._request(
|
f"/company/{company_id}/project",
|
||||||
"GET",
|
list_key="projectList",
|
||||||
f"/api/v1/projects/{project_id}/releases/{release_id}/assets",
|
)
|
||||||
|
|
||||||
|
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",
|
||||||
)
|
)
|
||||||
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 download_file(self, url: str, local_path: Path) -> Path:
|
def download_file(self, url: str, local_path: Path) -> Path:
|
||||||
"""Скачать файл по ссылке."""
|
"""Скачать файл по ссылке."""
|
||||||
@@ -298,7 +305,7 @@ class GitFlicClient:
|
|||||||
|
|
||||||
headers = {}
|
headers = {}
|
||||||
if url.startswith(self.base_url):
|
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)
|
logger.info("Скачивание: %s -> %s", url, local_path)
|
||||||
resp = self._session.get(url, headers=headers, stream=True,
|
resp = self._session.get(url, headers=headers, stream=True,
|
||||||
@@ -314,19 +321,6 @@ class GitFlicClient:
|
|||||||
local_path, local_path.stat().st_size)
|
local_path, local_path.stat().st_size)
|
||||||
return local_path
|
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
|
# Клиент PT Sandbox
|
||||||
@@ -422,7 +416,7 @@ def scan_new_releases(gf: GitFlicClient, sb: PTSandboxClient,
|
|||||||
1. Получить проекты
|
1. Получить проекты
|
||||||
2. Для каждого проекта релизы
|
2. Для каждого проекта релизы
|
||||||
3. Отфильтровать отправленные
|
3. Отфильтровать отправленные
|
||||||
4. Скачать файлы (архив + assets)
|
4. Скачать файлы (attachmentFiles из релиза)
|
||||||
5. Отправить в Sandbox
|
5. Отправить в Sandbox
|
||||||
6. Обновить состояние
|
6. Обновить состояние
|
||||||
"""
|
"""
|
||||||
@@ -437,25 +431,32 @@ def scan_new_releases(gf: GitFlicClient, sb: PTSandboxClient,
|
|||||||
|
|
||||||
for project in projects:
|
for project in projects:
|
||||||
pid = project.get("id")
|
pid = project.get("id")
|
||||||
pname = project.get("path_with_namespace") or project.get("name", str(pid))
|
owner_alias = project.get("owner", {}).get("alias", "")
|
||||||
logger.info("Обработка проекта: %s (id=%s)", pname, pid)
|
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:
|
try:
|
||||||
releases = gf.get_releases(pid)
|
releases = gf.get_releases(owner_alias, project_alias)
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
logger.error("Пропускаем проект %s - ошибка получения релизов", pname)
|
logger.error("Пропускаем проект %s - ошибка получения релизов", pname)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not releases:
|
if not releases:
|
||||||
|
logger.info("Нет релизов в проекте %s", pname)
|
||||||
continue
|
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":
|
if scan_mode == "latest":
|
||||||
rel = releases[0]
|
rel = releases[0]
|
||||||
rel_id = rel.get("id")
|
rel_id = rel.get("id")
|
||||||
rel_tag = rel.get("tag_name", "")
|
rel_tag = rel.get("tagName", "")
|
||||||
rel_version = rel.get("name", "")
|
rel_version = rel.get("title", "")
|
||||||
|
|
||||||
if is_release_sent(state, pid, rel_id, rel_tag, rel_version):
|
if is_release_sent(state, pid, rel_id, rel_tag, rel_version):
|
||||||
logger.info("Последний релиз уже отправлен. Пропускаем.")
|
logger.info("Последний релиз уже отправлен. Пропускаем.")
|
||||||
@@ -465,7 +466,7 @@ def scan_new_releases(gf: GitFlicClient, sb: PTSandboxClient,
|
|||||||
releases_to_process = [
|
releases_to_process = [
|
||||||
r for r in releases
|
r for r in releases
|
||||||
if not is_release_sent(state, pid, r.get("id"),
|
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:
|
if not releases_to_process:
|
||||||
logger.info("Нет новых релизов в проекте %s", pname)
|
logger.info("Нет новых релизов в проекте %s", pname)
|
||||||
@@ -473,32 +474,26 @@ def scan_new_releases(gf: GitFlicClient, sb: PTSandboxClient,
|
|||||||
|
|
||||||
for rel in releases_to_process:
|
for rel in releases_to_process:
|
||||||
rel_id = rel.get("id")
|
rel_id = rel.get("id")
|
||||||
rel_tag = rel.get("tag_name", "")
|
rel_tag = rel.get("tagName", "")
|
||||||
rel_version = rel.get("name", "")
|
rel_version = rel.get("title", "")
|
||||||
rel_key = make_release_key(pid, rel_id, rel_tag, rel_version)
|
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)
|
rel_key, rel_tag, rel_version)
|
||||||
|
|
||||||
# Собираем файлы для скачивания
|
# Собираем файлы для скачивания (attachmentFiles из релиза)
|
||||||
files_to_download: List[Dict[str, Any]] = []
|
files_to_download: List[Dict[str, Any]] = []
|
||||||
local_files: List[Dict[str, Any]] = []
|
local_files: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
# 1. Архив исходного кода по тегу
|
assets = rel.get("attachmentFiles", [])
|
||||||
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})
|
|
||||||
|
|
||||||
# 2. Assets (прикреплённые файлы)
|
|
||||||
try:
|
|
||||||
assets = gf.get_release_assets(pid, rel_id)
|
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
aname = asset.get("name") or asset.get("filename", "")
|
aname = asset.get("name", "")
|
||||||
if not aname:
|
aurl = asset.get("link", "")
|
||||||
|
if not aname or not aurl:
|
||||||
continue
|
continue
|
||||||
aurl = gf.get_release_asset_download_url(pid, rel_id, aname)
|
|
||||||
files_to_download.append({"type": "asset", "url": aurl, "filename": 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:
|
for fi in files_to_download:
|
||||||
|
|||||||
Reference in new issue
Block a user