forked from Lesin/reposync
288 lines
9.8 KiB
Python
288 lines
9.8 KiB
Python
#!/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 |