reposync/issue_sync_module/clients/github_client.py

288 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GitHub Issue API 客户端
"""
import requests
import json
import os
from typing import List, Dict, Optional
# 兼容导入优先使用环境变量后备使用src模块
try:
from src.base import config
from src.utils.logger import logger
except ImportError:
# 独立运行时使用环境变量和内置日志
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
config = None
class GitHubIssueClient:
"""GitHub Issue操作客户端"""
def __init__(self, organization: str, repo_name: str):
self.organization = organization
self.repo_name = repo_name
# 获取GitHub Token和API地址
if config:
self.token = config.ACCOUNT.get('github_token', '')
self.base_url = config.GITHUB_ENV.get('github_api_address', 'https://api.github.com/repos')
else:
# 从环境变量获取
self.token = os.getenv('GITHUB_TOKEN', '')
self.base_url = os.getenv('GITHUB_API_HOST', 'https://api.github.com/repos')
self.headers = {
'Authorization': f'token {self.token}',
'Accept': 'application/vnd.github+json'
}
def get_issues(self, state: str = 'all') -> List[Dict]:
"""获取仓库的Issue列表"""
url = f"{self.base_url}/{self.organization}/{self.repo_name}/issues"
params = {
'state': state,
'per_page': 100
}
try:
response = requests.get(url, headers=self.headers, params=params, verify=False)
response.raise_for_status()
issues = response.json()
# 过滤掉Pull Request (GitHub API中PR也会在issues接口返回)
filtered_issues = [issue for issue in issues if 'pull_request' not in issue]
logger.info(f"获取到GitHub仓库 {self.organization}/{self.repo_name}{len(filtered_issues)} 个Issue")
return filtered_issues
except Exception as e:
logger.error(f"获取GitHub Issues失败: {str(e)}")
return []
def get_issue(self, issue_number: int) -> Optional[Dict]:
"""获取单个Issue详情"""
url = f"{self.base_url}/{self.organization}/{self.repo_name}/issues/{issue_number}"
try:
response = requests.get(url, headers=self.headers, verify=False)
response.raise_for_status()
issue = response.json()
logger.info(f"获取GitHub Issue #{issue_number} 成功")
return issue
except Exception as e:
logger.error(f"获取GitHub Issue #{issue_number} 失败: {str(e)}")
return None
def create_issue(self, title: str, body: str = "", labels: List[str] = None,
assignees: List[str] = None) -> Optional[Dict]:
"""创建新Issue"""
url = f"{self.base_url}/{self.organization}/{self.repo_name}/issues"
data = {
"title": title,
"body": body or ""
}
if labels:
data["labels"] = labels
if assignees:
data["assignees"] = assignees
try:
response = requests.post(url, headers=self.headers, json=data, verify=False)
response.raise_for_status()
issue = response.json()
logger.info(f"创建GitHub Issue成功: #{issue['number']} - {title}")
return issue
except Exception as e:
logger.error(f"创建GitHub Issue失败: {str(e)}")
return None
def update_issue(self, issue_number: int, title: str = None, body: str = None,
state: str = None, labels: List[str] = None,
assignees: List[str] = None) -> Optional[Dict]:
"""更新Issue"""
url = f"{self.base_url}/{self.organization}/{self.repo_name}/issues/{issue_number}"
data = {}
if title is not None:
data["title"] = title
if body is not None:
data["body"] = body
if state is not None:
data["state"] = state
if labels is not None:
data["labels"] = labels
if assignees is not None:
data["assignees"] = assignees
try:
response = requests.patch(url, headers=self.headers, json=data, verify=False)
response.raise_for_status()
issue = response.json()
logger.info(f"更新GitHub Issue #{issue_number} 成功")
return issue
except Exception as e:
logger.error(f"更新GitHub Issue #{issue_number} 失败: {str(e)}")
return None
def close_issue(self, issue_number: int) -> bool:
"""关闭Issue"""
return self.update_issue(issue_number, state="closed") is not None
def delete_issue(self, issue_number: int) -> bool:
"""删除Issue (GitHub不支持删除只能关闭)"""
logger.warning("GitHub不支持删除Issue将关闭该Issue")
return self.close_issue(issue_number)
def find_issue_by_title(self, title: str) -> Optional[Dict]:
"""根据标题查找Issue避免重复创建"""
issues = self.get_issues()
for issue in issues:
if issue.get('title', '') == title:
return issue
return None
# =============== 评论相关API ===============
def get_issue_comments(self, issue_number: int, per_page: int = 100) -> List[Dict]:
"""获取Issue的评论列表
Args:
issue_number: Issue编号
per_page: 每页数量
Returns:
评论列表
"""
url = f"{self.base_url}/{self.organization}/{self.repo_name}/issues/{issue_number}/comments"
params = {
'per_page': per_page
}
try:
response = requests.get(url, headers=self.headers, params=params, verify=False)
response.raise_for_status()
comments = response.json()
logger.info(f"获取到GitHub Issue #{issue_number}{len(comments)} 条评论")
return comments
except Exception as e:
logger.error(f"获取GitHub Issue #{issue_number} 评论失败: {str(e)}")
return []
def get_issue_comment(self, comment_id: int) -> Optional[Dict]:
"""获取单个评论详情
Args:
comment_id: 评论ID
Returns:
评论对象
"""
url = f"{self.base_url}/{self.organization}/{self.repo_name}/issues/comments/{comment_id}"
try:
response = requests.get(url, headers=self.headers, verify=False)
response.raise_for_status()
comment = response.json()
logger.info(f"获取GitHub评论 #{comment_id} 成功")
return comment
except Exception as e:
logger.error(f"获取GitHub评论 #{comment_id} 失败: {str(e)}")
return None
def create_issue_comment(self, issue_number: int, body: str) -> Optional[Dict]:
"""创建Issue评论
Args:
issue_number: Issue编号
body: 评论内容
Returns:
创建的评论对象
"""
url = f"{self.base_url}/{self.organization}/{self.repo_name}/issues/{issue_number}/comments"
data = {
"body": body
}
try:
response = requests.post(url, headers=self.headers, json=data, verify=False)
response.raise_for_status()
comment = response.json()
comment_content = comment.get('body', '')[:50] + ('...' if len(comment.get('body', '')) > 50 else '')
logger.info(f"✅ 成功创建GitHub Issue #{issue_number} 评论: #{comment['id']} - '{comment_content}'")
return comment
except Exception as e:
logger.error(f"创建GitHub Issue #{issue_number} 评论失败: {str(e)}")
return None
def update_issue_comment(self, comment_id: int, body: str) -> Optional[Dict]:
"""更新评论内容
Args:
comment_id: 评论ID
body: 新的评论内容
Returns:
更新后的评论对象
"""
url = f"{self.base_url}/{self.organization}/{self.repo_name}/issues/comments/{comment_id}"
data = {
"body": body
}
try:
response = requests.patch(url, headers=self.headers, json=data, verify=False)
response.raise_for_status()
comment = response.json()
logger.info(f"更新GitHub评论 #{comment_id} 成功")
return comment
except Exception as e:
logger.error(f"更新GitHub评论 #{comment_id} 失败: {str(e)}")
return None
def delete_issue_comment(self, comment_id: int) -> bool:
"""删除评论
Args:
comment_id: 评论ID
Returns:
是否删除成功
"""
url = f"{self.base_url}/{self.organization}/{self.repo_name}/issues/comments/{comment_id}"
try:
response = requests.delete(url, headers=self.headers, verify=False)
response.raise_for_status()
logger.info(f"删除GitHub评论 #{comment_id} 成功")
return True
except Exception as e:
logger.error(f"删除GitHub评论 #{comment_id} 失败: {str(e)}")
return False