""" 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)}