File size: 3,433 Bytes
91990f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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}")