MCPilot / src /simulators /github.py
girish-hari's picture
checking-in the project source code
2358888
"""
GitHub API simulator.
Provides realistic mock responses for GitHub API actions.
"""
from typing import Optional
from .base import BaseSimulator
from datetime import datetime
import httpx
class GitHubSimulator(BaseSimulator):
"""Simulator for GitHub API."""
def __init__(self):
super().__init__('github')
self.base_url = "https://api.github.com"
def load_mock_responses(self):
"""Load GitHub mock response templates."""
self.mock_responses = {
'get_pull_request': self._get_pr_template,
'create_pull_request': self._create_pr_template,
'add_comment': self._add_comment_template,
'create_issue': self._create_issue_template,
'list_pull_requests': self._list_prs_template,
}
def get_required_permissions(self, action: str) -> set[str]:
"""Get required GitHub scopes for an action."""
permissions_map = {
'get_pull_request': {'repo'},
'create_pull_request': {'repo'},
'add_comment': {'repo'},
'create_issue': {'repo'},
'list_pull_requests': {'repo'},
}
return permissions_map.get(action, set())
def validate_params(self, action: str, params: dict) -> tuple[bool, Optional[str]]:
"""Validate parameters for GitHub actions."""
if action == 'get_pull_request':
required = {'owner', 'repo', 'number'}
missing = required - set(params.keys())
if missing:
return False, f"Missing required parameters: {missing}"
if not isinstance(params['number'], int):
return False, "Parameter 'number' must be an integer"
elif action == 'create_pull_request':
required = {'owner', 'repo', 'title', 'head', 'base'}
missing = required - set(params.keys())
if missing:
return False, f"Missing required parameters: {missing}"
elif action == 'add_comment':
required = {'owner', 'repo', 'issue_number', 'body'}
missing = required - set(params.keys())
if missing:
return False, f"Missing required parameters: {missing}"
elif action == 'create_issue':
required = {'owner', 'repo', 'title'}
missing = required - set(params.keys())
if missing:
return False, f"Missing required parameters: {missing}"
elif action == 'list_pull_requests':
required = {'owner', 'repo'}
missing = required - set(params.keys())
if missing:
return False, f"Missing required parameters: {missing}"
else:
return False, f"Unknown action: {action}"
return True, None
def generate_mock_response(self, action: str, params: dict) -> dict:
"""Generate realistic GitHub API response."""
if action not in self.mock_responses:
raise ValueError(f"Unknown action: {action}")
template_func = self.mock_responses[action]
return template_func(params)
def _get_pr_template(self, params: dict) -> dict:
"""Mock response for getting a pull request."""
return {
"url": f"https://api.github.com/repos/{params['owner']}/{params['repo']}/pulls/{params['number']}",
"id": 1234567890,
"node_id": "PR_kwDOAB1CkM5ZGxyz",
"html_url": f"https://github.com/{params['owner']}/{params['repo']}/pull/{params['number']}",
"number": params['number'],
"state": "open",
"locked": False,
"title": "Add new feature for improved performance",
"user": {
"login": "developer123",
"id": 12345,
"avatar_url": "https://avatars.githubusercontent.com/u/12345?v=4",
"type": "User"
},
"body": "This PR implements the new caching mechanism to improve API response times by 40%.\n\n## Changes\n- Added Redis caching layer\n- Updated documentation\n- Added tests\n\n## Testing\nAll tests pass locally.",
"created_at": "2025-11-28T10:30:00Z",
"updated_at": "2025-11-30T14:22:00Z",
"closed_at": None,
"merged_at": None,
"merge_commit_sha": None,
"assignee": None,
"assignees": [],
"requested_reviewers": [
{
"login": "reviewer1",
"id": 67890,
"type": "User"
}
],
"labels": [
{
"id": 111,
"name": "enhancement",
"color": "84b6eb"
},
{
"id": 222,
"name": "performance",
"color": "0e8a16"
}
],
"milestone": None,
"draft": False,
"commits": 5,
"additions": 250,
"deletions": 42,
"changed_files": 8,
"head": {
"label": f"{params['owner']}:feature-branch",
"ref": "feature-branch",
"sha": "abc123def456"
},
"base": {
"label": f"{params['owner']}:main",
"ref": "main",
"sha": "def456abc123"
},
"mergeable": True,
"mergeable_state": "clean",
"comments": 3,
"review_comments": 5,
"commits_url": f"https://api.github.com/repos/{params['owner']}/{params['repo']}/pulls/{params['number']}/commits",
"statuses_url": f"https://api.github.com/repos/{params['owner']}/{params['repo']}/statuses/abc123def456"
}
def _create_pr_template(self, params: dict) -> dict:
"""Mock response for creating a pull request."""
return {
"url": f"https://api.github.com/repos/{params['owner']}/{params['repo']}/pulls/1",
"id": 1234567891,
"number": 1,
"state": "open",
"title": params['title'],
"user": {
"login": "api-user",
"id": 99999,
"type": "User"
},
"body": params.get('body', ''),
"created_at": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
"updated_at": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
"head": {
"ref": params['head'],
"sha": "new123abc456"
},
"base": {
"ref": params['base'],
"sha": "base456def789"
},
"html_url": f"https://github.com/{params['owner']}/{params['repo']}/pull/1",
"mergeable": None,
"draft": params.get('draft', False)
}
def _add_comment_template(self, params: dict) -> dict:
"""Mock response for adding a comment."""
return {
"id": 987654321,
"node_id": "IC_kwDOAB1CkM5ZGxyz",
"url": f"https://api.github.com/repos/{params['owner']}/{params['repo']}/issues/comments/987654321",
"html_url": f"https://github.com/{params['owner']}/{params['repo']}/issues/{params['issue_number']}#issuecomment-987654321",
"body": params['body'],
"user": {
"login": "api-user",
"id": 99999,
"type": "User"
},
"created_at": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
"updated_at": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
"issue_url": f"https://api.github.com/repos/{params['owner']}/{params['repo']}/issues/{params['issue_number']}"
}
def _create_issue_template(self, params: dict) -> dict:
"""Mock response for creating an issue."""
return {
"id": 1111111111,
"number": 42,
"state": "open",
"title": params['title'],
"body": params.get('body', ''),
"user": {
"login": "api-user",
"id": 99999,
"type": "User"
},
"labels": [],
"assignees": [],
"comments": 0,
"created_at": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
"updated_at": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
"html_url": f"https://github.com/{params['owner']}/{params['repo']}/issues/42",
"url": f"https://api.github.com/repos/{params['owner']}/{params['repo']}/issues/42"
}
def _list_prs_template(self, params: dict) -> dict:
"""Mock response for listing pull requests."""
return [
{
"id": 1234567890,
"number": 1,
"state": params.get('state', 'open'),
"title": "Feature: Add caching layer",
"user": {"login": "dev1", "id": 111},
"created_at": "2025-11-28T10:00:00Z",
"updated_at": "2025-11-30T12:00:00Z",
"html_url": f"https://github.com/{params['owner']}/{params['repo']}/pull/1"
},
{
"id": 1234567891,
"number": 2,
"state": params.get('state', 'open'),
"title": "Fix: Resolve memory leak",
"user": {"login": "dev2", "id": 222},
"created_at": "2025-11-29T14:00:00Z",
"updated_at": "2025-11-30T11:00:00Z",
"html_url": f"https://github.com/{params['owner']}/{params['repo']}/pull/2"
}
]
def _execute_real(self, action: str, params: dict, credentials: dict) -> dict:
"""Execute real GitHub API call."""
token = credentials.get('token')
if not token:
return {'success': False, 'error': 'MISSING_TOKEN', 'message': 'GitHub token required'}
headers = {
'Authorization': f'Bearer {token}',
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
}
try:
if action == 'get_pull_request':
url = f"{self.base_url}/repos/{params['owner']}/{params['repo']}/pulls/{params['number']}"
response = httpx.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return {'success': True, 'mode': 'real', 'service': self.service_name, 'action': action, 'data': response.json(), 'note': '๐ŸŒ Real API call'}
elif action == 'list_pull_requests':
url = f"{self.base_url}/repos/{params['owner']}/{params['repo']}/pulls"
response = httpx.get(url, headers=headers, params={'state': params.get('state', 'open')}, timeout=30.0)
response.raise_for_status()
return {'success': True, 'mode': 'real', 'service': self.service_name, 'action': action, 'data': response.json(), 'note': '๐ŸŒ Real API call'}
else:
return {'success': False, 'error': 'NOT_IMPLEMENTED', 'message': f'Real API not implemented for: {action}'}
except httpx.HTTPStatusError as e:
return {'success': False, 'error': 'API_ERROR', 'message': f'GitHub API error: {e.response.status_code}', 'detail': e.response.text}
except Exception as e:
return {'success': False, 'error': 'UNKNOWN_ERROR', 'message': str(e)}