feat: add Gitea integration service and API endpoints
This commit is contained in:
parent
478d72d00a
commit
8f130b2cbd
|
|
@ -6,6 +6,7 @@ from .integrations import router as integrations_router
|
||||||
from .issues import router as issues_router
|
from .issues import router as issues_router
|
||||||
from .webhooks import router as webhooks_router
|
from .webhooks import router as webhooks_router
|
||||||
from .reports import router as reports_router
|
from .reports import router as reports_router
|
||||||
|
from .gitea import router as gitea_router
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(auth_router, prefix="/auth", tags=["Authentication"])
|
api_router.include_router(auth_router, prefix="/auth", tags=["Authentication"])
|
||||||
|
|
@ -15,3 +16,4 @@ api_router.include_router(integrations_router, prefix="/integrations", tags=["In
|
||||||
api_router.include_router(issues_router, prefix="/issues", tags=["Issues"])
|
api_router.include_router(issues_router, prefix="/issues", tags=["Issues"])
|
||||||
api_router.include_router(webhooks_router, prefix="/webhooks", tags=["Webhooks"])
|
api_router.include_router(webhooks_router, prefix="/webhooks", tags=["Webhooks"])
|
||||||
api_router.include_router(reports_router, prefix="/reports", tags=["Reports"])
|
api_router.include_router(reports_router, prefix="/reports", tags=["Reports"])
|
||||||
|
api_router.include_router(gitea_router, prefix="/gitea", tags=["Gitea"])
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
"""Gitea integration endpoints."""
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.models.integration import Integration, IntegrationType
|
||||||
|
from app.models.organization import OrganizationMember
|
||||||
|
from app.api.deps import require_role
|
||||||
|
from app.services.gitea import GiteaService
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/repos", response_model=List[Dict[str, Any]])
|
||||||
|
async def list_repositories(
|
||||||
|
org_id: int,
|
||||||
|
member: OrganizationMember = Depends(require_role("viewer")),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""List Gitea repositories for organization."""
|
||||||
|
# Get Gitea integration
|
||||||
|
result = await db.execute(
|
||||||
|
select(Integration)
|
||||||
|
.where(Integration.organization_id == org_id)
|
||||||
|
.where(Integration.type == IntegrationType.GITLAB) # Using GITLAB as Gitea
|
||||||
|
.where(Integration.status == "ACTIVE")
|
||||||
|
)
|
||||||
|
integration = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not integration or not integration.base_url or not integration.api_key:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Gitea integration not configured"
|
||||||
|
)
|
||||||
|
|
||||||
|
gitea = GiteaService(integration.base_url, integration.api_key)
|
||||||
|
repos = await gitea.list_repositories("startdata") # Fixed owner for now
|
||||||
|
|
||||||
|
return repos
|
||||||
|
|
||||||
|
@router.get("/repos/{owner}/{repo}")
|
||||||
|
async def get_repository(
|
||||||
|
org_id: int,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
member: OrganizationMember = Depends(require_role("viewer")),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Get repository details."""
|
||||||
|
result = await db.execute(
|
||||||
|
select(Integration)
|
||||||
|
.where(Integration.organization_id == org_id)
|
||||||
|
.where(Integration.type == IntegrationType.GITLAB)
|
||||||
|
.where(Integration.status == "ACTIVE")
|
||||||
|
)
|
||||||
|
integration = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not integration or not integration.base_url or not integration.api_key:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Gitea integration not configured"
|
||||||
|
)
|
||||||
|
|
||||||
|
gitea = GiteaService(integration.base_url, integration.api_key)
|
||||||
|
repo_data = await gitea.get_repo(owner, repo)
|
||||||
|
|
||||||
|
if not repo_data:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Repository not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return repo_data
|
||||||
|
|
||||||
|
@router.get("/repos/{owner}/{repo}/file")
|
||||||
|
async def get_file(
|
||||||
|
org_id: int,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
path: str,
|
||||||
|
ref: str = "main",
|
||||||
|
member: OrganizationMember = Depends(require_role("viewer")),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Get file content from repository."""
|
||||||
|
result = await db.execute(
|
||||||
|
select(Integration)
|
||||||
|
.where(Integration.organization_id == org_id)
|
||||||
|
.where(Integration.type == IntegrationType.GITLAB)
|
||||||
|
.where(Integration.status == "ACTIVE")
|
||||||
|
)
|
||||||
|
integration = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not integration or not integration.base_url or not integration.api_key:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Gitea integration not configured"
|
||||||
|
)
|
||||||
|
|
||||||
|
gitea = GiteaService(integration.base_url, integration.api_key)
|
||||||
|
content = await gitea.get_file(owner, repo, path, ref)
|
||||||
|
|
||||||
|
if content is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="File not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"path": path, "content": content, "ref": ref}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
"""Gitea integration service."""
|
||||||
|
import httpx
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
|
||||||
|
class GiteaService:
|
||||||
|
def __init__(self, base_url: str, token: str):
|
||||||
|
self.base_url = base_url.rstrip('/')
|
||||||
|
self.token = token
|
||||||
|
self.headers = {
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_repo(self, owner: str, repo: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get repository details."""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
response = await client.get(
|
||||||
|
f"{self.base_url}/api/v1/repos/{owner}/{repo}",
|
||||||
|
headers=self.headers,
|
||||||
|
timeout=10.0
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_file(self, owner: str, repo: str, path: str, ref: str = "main") -> Optional[str]:
|
||||||
|
"""Get file content from repository."""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
response = await client.get(
|
||||||
|
f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}?ref={ref}",
|
||||||
|
headers=self.headers,
|
||||||
|
timeout=10.0
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
# Gitea returns base64 encoded content
|
||||||
|
import base64
|
||||||
|
return base64.b64decode(data.get("content", "")).decode("utf-8")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def create_branch(self, owner: str, repo: str, branch: str, from_branch: str = "main") -> bool:
|
||||||
|
"""Create a new branch."""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
response = await client.post(
|
||||||
|
f"{self.base_url}/api/v1/repos/{owner}/{repo}/branches",
|
||||||
|
headers=self.headers,
|
||||||
|
json={"new_branch_name": branch, "old_branch_name": from_branch},
|
||||||
|
timeout=10.0
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def update_file(self, owner: str, repo: str, path: str, content: str,
|
||||||
|
message: str, branch: str, sha: Optional[str] = None) -> bool:
|
||||||
|
"""Update file in repository."""
|
||||||
|
import base64
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"content": base64.b64encode(content.encode()).decode(),
|
||||||
|
"message": message,
|
||||||
|
"branch": branch
|
||||||
|
}
|
||||||
|
if sha:
|
||||||
|
payload["sha"] = sha
|
||||||
|
|
||||||
|
response = await client.put(
|
||||||
|
f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}",
|
||||||
|
headers=self.headers,
|
||||||
|
json=payload,
|
||||||
|
timeout=10.0
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def create_pull_request(self, owner: str, repo: str, title: str,
|
||||||
|
body: str, head: str, base: str = "main") -> Optional[str]:
|
||||||
|
"""Create a pull request."""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
response = await client.post(
|
||||||
|
f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls",
|
||||||
|
headers=self.headers,
|
||||||
|
json={
|
||||||
|
"title": title,
|
||||||
|
"body": body,
|
||||||
|
"head": head,
|
||||||
|
"base": base
|
||||||
|
},
|
||||||
|
timeout=10.0
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
pr_data = response.json()
|
||||||
|
return pr_data.get("html_url")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def list_repositories(self, owner: str) -> List[Dict[str, Any]]:
|
||||||
|
"""List repositories for owner."""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
response = await client.get(
|
||||||
|
f"{self.base_url}/api/v1/users/{owner}/repos",
|
||||||
|
headers=self.headers,
|
||||||
|
timeout=10.0
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
@ -10,3 +10,4 @@ bcrypt==3.2.2
|
||||||
httpx==0.26.0
|
httpx==0.26.0
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
email-validator>=2.0.0
|
email-validator>=2.0.0
|
||||||
|
httpx==0.25.2
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue