import httpx import logging from config.settings import get_settings logger = logging.getLogger(__name__) settings = get_settings() JDOODLE_LANGUAGE_MAP = { "python": {"language": "python3", "versionIndex": "4"}, "javascript": {"language": "nodejs", "versionIndex": "4"}, "typescript": {"language": "typescript", "versionIndex": "1"}, "java": {"language": "java", "versionIndex": "4"}, "c++": {"language": "cpp17", "versionIndex": "1"}, "c": {"language": "c", "versionIndex": "5"}, "go": {"language": "go", "versionIndex": "4"}, "rust": {"language": "rust", "versionIndex": "4"}, "ruby": {"language": "ruby", "versionIndex": "4"}, "php": {"language": "php", "versionIndex": "4"}, } async def run_code_jdoodle(language: str, code: str) -> str: """Run code via JDoodle API. Returns output string.""" lang_config = JDOODLE_LANGUAGE_MAP.get(language.lower()) if not lang_config: raise ValueError(f"Language '{language}' not supported") async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( "https://api.jdoodle.com/v1/execute", json={ "clientId": settings.jdoodle_client_id, "clientSecret": settings.jdoodle_client_secret, "script": code, "language": lang_config["language"], "versionIndex": lang_config["versionIndex"], } ) if response.status_code != 200: logger.error(f"JDoodle error {response.status_code}: {response.text}") raise Exception(f"Code execution service error: {response.status_code}") result = response.json() # JDoodle returns error info in statusCode field if result.get("statusCode") == 400: raise Exception("JDoodle: Invalid request") if result.get("statusCode") == 429: raise Exception("Daily code execution limit reached. Try again tomorrow.") return result.get("output", "No output") async def run_code_piston(language: str, version: str, code: str) -> str: """Run code via Piston API (fallback). Returns output string.""" async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( f"{settings.piston_api_url}/execute", json={ "language": language, "version": version, "files": [{"content": code}], } ) if response.status_code != 200: raise Exception(f"Piston error: {response.status_code}") result = response.json() run = result.get("run", {}) return run.get("output") or run.get("stderr") or "No output" async def run_code(language: str, code: str, version: str = "") -> str: """Run code with JDoodle primary, Piston fallback.""" try: return await run_code_jdoodle(language, code) except Exception as e: logger.warning(f"JDoodle failed ({e}), falling back to Piston") try: return await run_code_piston(language, version, code) except Exception as e2: raise Exception(f"Both code runners failed. JDoodle: {e} | Piston: {e2}")