Skip to content

Commit cc616ee

Browse files
committed
more language-agnostic API: instead of python_version -> lang+lang_version [still only python supported]
Also corresponding changes in frontend
1 parent 7a41f55 commit cc616ee

12 files changed

Lines changed: 155 additions & 65 deletions

File tree

backend/app/api/routes/execution.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,26 @@ async def create_execution(
2929
logger.info(
3030
"Received script execution request",
3131
extra={
32-
"python_version": execution.python_version,
32+
"lang": execution.lang,
33+
"lang_version": execution.lang_version,
3334
"script_length": len(execution.script),
3435
"client_ip": get_remote_address(request),
3536
"endpoint": "/execute",
3637
},
3738
)
3839

39-
try:
40-
python_version = execution.python_version or "3.11"
40+
lang_and_version: str = execution.lang + "-" + execution.lang_version
4141

42-
with EXECUTION_DURATION.labels(python_version=python_version).time():
42+
try:
43+
with EXECUTION_DURATION.labels(lang_and_version=lang_and_version).time():
4344
result = await execution_service.execute_script(
44-
execution.script, python_version
45+
script=execution.script,
46+
lang=execution.lang,
47+
lang_version=execution.lang_version
4548
)
4649

4750
SCRIPT_EXECUTIONS.labels(
48-
status="success", python_version=python_version
51+
status="success", lang_and_version=lang_and_version
4952
).inc()
5053

5154
logger.info(
@@ -56,7 +59,7 @@ async def create_execution(
5659

5760
except IntegrationException as e:
5861
SCRIPT_EXECUTIONS.labels(
59-
status="integration_error", python_version=execution.python_version or "3.11"
62+
status="integration_error", lang_and_version=lang_and_version
6063
).inc()
6164

6265
logger.error(
@@ -65,22 +68,22 @@ async def create_execution(
6568
"error_type": "IntegrationException",
6669
"status_code": e.status_code,
6770
"error_detail": e.detail,
68-
"python_version": execution.python_version,
71+
"lang_and_version": lang_and_version,
6972
},
7073
)
7174
raise HTTPException(status_code=e.status_code, detail=e.detail) from e
7275

7376
except Exception as e:
7477
SCRIPT_EXECUTIONS.labels(
75-
status="error", python_version=execution.python_version or "3.11"
78+
status="error", lang_and_version=lang_and_version
7679
).inc()
7780

7881
logger.error(
7982
"Unexpected error during script execution",
8083
extra={
8184
"error_type": type(e).__name__,
8285
"error_detail": str(e),
83-
"python_version": execution.python_version,
86+
"lang_and_version": lang_and_version,
8487
},
8588
)
8689
raise HTTPException(status_code=400, detail=f"Error during execution: {str(e)}") from e
@@ -117,7 +120,8 @@ async def get_result(
117120
extra={
118121
"execution_id": result.id,
119122
"status": result.status,
120-
"python_version": result.python_version,
123+
"lang": result.lang,
124+
"lang_version": result.lang_version,
121125
"has_errors": bool(result.errors),
122126
"resource_usage": result.resource_usage,
123127
},
@@ -133,7 +137,8 @@ async def get_result(
133137
status=result.status,
134138
output=result.output,
135139
errors=result.errors,
136-
python_version=result.python_version,
140+
lang=result.lang,
141+
lang_version=result.lang_version,
137142
resource_usage=resource_usage_obj,
138143
)
139144

backend/app/config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ class Settings(BaseSettings):
2323
K8S_POD_CPU_REQUEST: str = "100m"
2424
K8S_POD_MEMORY_REQUEST: str = "128Mi"
2525
K8S_POD_EXECUTION_TIMEOUT: int = 5 # in seconds
26-
SUPPORTED_PYTHON_VERSIONS: list[str] = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
26+
27+
SUPPORTED_RUNTIMES: dict[str, list[str]] = {
28+
"python": ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
29+
}
30+
2731
PROMETHEUS_URL: str = "http://prometheus:9090"
2832

2933
TESTING: bool = False

backend/app/core/metrics.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@
1111
class BoundedLabelsCounter(Counter):
1212
def labels(self, *args: Any, **kwargs: Any) -> 'BoundedLabelsCounter':
1313
# Validate labels before creating new time series
14-
if (
15-
"python_version" in kwargs
16-
and kwargs["python_version"] not in get_settings().SUPPORTED_PYTHON_VERSIONS
17-
):
18-
kwargs["python_version"] = "unknown"
1914
if "status" in kwargs and kwargs["status"] not in ALLOWED_STATUSES:
2015
kwargs["status"] = "unknown"
2116
return super().labels(*args, **kwargs)
@@ -25,13 +20,13 @@ def labels(self, *args: Any, **kwargs: Any) -> 'BoundedLabelsCounter':
2520
SCRIPT_EXECUTIONS = BoundedLabelsCounter(
2621
"script_executions_total",
2722
"Total number of script executions",
28-
["status", "python_version"],
23+
["status", "lang_and_version"],
2924
)
3025

3126
EXECUTION_DURATION = Histogram(
3227
"script_execution_duration_seconds",
3328
"Time spent executing scripts",
34-
["python_version"],
29+
["lang_and_version"],
3530
buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0], # Define specific buckets
3631
)
3732

@@ -41,7 +36,7 @@ def labels(self, *args: Any, **kwargs: Any) -> 'BoundedLabelsCounter':
4136

4237
# Resource usage metrics
4338
MEMORY_USAGE = Gauge(
44-
"script_memory_usage_bytes", "Memory usage per script execution", ["python_version"]
39+
"script_memory_usage_bytes", "Memory usage per script execution", ["lang_and_version"]
4540
)
4641

4742
ERROR_COUNTER = Counter(

backend/app/schemas/execution.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ class ExecutionBase(BaseModel):
1414
status: str = "queued"
1515
output: Optional[str] = None
1616
errors: Optional[str] = None
17-
python_version: str = "3.11"
17+
lang: str = "python"
18+
lang_version: str = "3.11"
1819

1920

2021
class ExecutionCreate(ExecutionBase):
@@ -56,8 +57,11 @@ class ResourceUsage(BaseModel):
5657

5758
class ExecutionRequest(BaseModel):
5859
script: str
59-
python_version: Optional[str] = Field(
60-
default="3.11", description="Python version to use for execution"
60+
lang: str = Field(
61+
default="python", description="Language name"
62+
),
63+
lang_version: Optional[str] = Field(
64+
default="3.11", description="Language version to use for execution"
6165
)
6266

6367

@@ -74,7 +78,8 @@ class ExecutionResult(BaseModel):
7478
status: str
7579
output: Optional[str] = None
7680
errors: Optional[str] = None
77-
python_version: str
81+
lang: str
82+
lang_version: str
7883
resource_usage: Optional[ResourceUsage] = None
7984

8085
class Config:
@@ -87,4 +92,4 @@ class K8SResourceLimits(BaseModel):
8792
cpu_request: str
8893
memory_request: str
8994
execution_timeout: int
90-
supported_python_versions: List[str]
95+
supported_runtimes: dict[str, list[str]]

backend/app/services/execution_service.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,20 @@ async def get_k8s_resource_limits(self) -> Dict[str, Any]:
4848
"cpu_request": self.settings.K8S_POD_CPU_REQUEST,
4949
"memory_request": self.settings.K8S_POD_MEMORY_REQUEST,
5050
"execution_timeout": self.settings.K8S_POD_EXECUTION_TIMEOUT,
51-
"supported_python_versions": self.settings.SUPPORTED_PYTHON_VERSIONS,
51+
"supported_runtimes": self.settings.SUPPORTED_RUNTIMES,
5252
}
5353

5454
async def _start_k8s_execution(
55-
self, execution_id_str: str, script: str, python_version: str
55+
self, execution_id_str: str,
56+
script: str,
57+
lang: str,
58+
lang_version: str
5659
) -> None:
5760
try:
5861
# Python-specific configuration
59-
image = f"python:{python_version}-slim"
62+
image = f"{lang}:{lang_version}-slim"
63+
64+
# TODO: decouple from file format smh
6065
command = ["python", "/scripts/script.py"]
6166
config_map_data = {
6267
"script.py": script
@@ -169,36 +174,44 @@ async def _try_finalize_execution(self, execution: ExecutionInDB) -> Optional[Ex
169174
raise IntegrationException(status_code=500, detail="Failed to retrieve execution after update.")
170175

171176
status_label = "success" if updated_execution.status == ExecutionStatus.COMPLETED else "error"
172-
SCRIPT_EXECUTIONS.labels(status=status_label, python_version=updated_execution.python_version).inc()
177+
SCRIPT_EXECUTIONS.labels(status=status_label,
178+
lang_and_version=execution.lang + "-" + execution.lang_version).inc()
173179
if status_label == "error":
174180
ERROR_COUNTER.labels(error_type="ScriptExecutionError").inc()
175181

176182
return updated_execution
177183

178184
async def execute_script(
179-
self, script: str, python_version: str = "3.11"
185+
self, script: str,
186+
lang: str = "python",
187+
lang_version: str = "3.11"
180188
) -> ExecutionInDB:
181189
ACTIVE_EXECUTIONS.inc()
182190
start_time = time()
183191
inserted_oid = None
184192

185193
try:
186-
if python_version not in self.settings.SUPPORTED_PYTHON_VERSIONS:
187-
raise IntegrationException(status_code=400, detail=f"Unsupported Python version: {python_version}")
194+
if lang not in self.settings.SUPPORTED_RUNTIMES.keys():
195+
raise IntegrationException(status_code=400, detail=f"Language '{lang}' not supported.")
196+
197+
if lang_version not in self.settings.SUPPORTED_RUNTIMES[lang]:
198+
raise IntegrationException(status_code=400, detail=f"Language version '{lang_version}' not supported.")
188199

189200
execution_create = ExecutionCreate(
190201
script=script,
191-
python_version=python_version,
202+
lang=lang,
203+
lang_version=lang_version,
192204
status=ExecutionStatus.QUEUED,
193205
)
194206
execution_to_insert = ExecutionInDB(**execution_create.model_dump())
195207
inserted_oid = await self.execution_repo.create_execution(execution_to_insert)
196208
logger.info(f"Created execution record {inserted_oid} with status QUEUED.")
197209

198210
await self._start_k8s_execution(
199-
inserted_oid, script, python_version
211+
execution_id_str=inserted_oid, script=script,
212+
lang=lang, lang_version=lang_version
200213
)
201-
SCRIPT_EXECUTIONS.labels(status="initiated", python_version=python_version).inc()
214+
SCRIPT_EXECUTIONS.labels(status="initiated", lang_and_version=lang + "-" + lang_version).inc()
202215
await asyncio.sleep(0.1)
203216

204217
final_execution_state = await self.execution_repo.get_execution(inserted_oid)
@@ -218,7 +231,7 @@ async def execute_script(
218231
detail=f"Internal server error during script execution request: "
219232
f"{str(e)}") from e
220233
finally:
221-
EXECUTION_DURATION.labels(python_version=python_version).observe(time() - start_time)
234+
EXECUTION_DURATION.labels(lang_and_version=lang + "-" + lang_version).observe(time() - start_time)
222235
ACTIVE_EXECUTIONS.dec()
223236

224237
async def get_execution_result(self, execution_id: str) -> ExecutionInDB:

backend/grafana/provisioning/dashboards/integr8scode.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"editorMode": "code",
6868
"expr": "floor(increase(script_executions_total[5m]))",
6969
"instant": false,
70-
"legendFormat": "{{python_version}} {{status}}",
70+
"legendFormat": "{{lang_and_version}} {{status}}",
7171
"range": true,
7272
"refId": "A"
7373
}

backend/prometheus/prometheus.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ scrape_configs:
1010
static_configs:
1111
- targets: ['backend:443']
1212
metric_relabel_configs:
13-
- source_labels: [ python_version ]
14-
regex: '3\.(7|8|9|10|11|12)'
13+
- source_labels: [ lang_and_version ]
14+
regex: '(.+)-(.+)'
1515
action: keep
1616
- source_labels: [ status ]
1717
regex: '(success|error|timeout|invalid_input|unknown)'

backend/tests/integration/test_execution_api.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ async def test_execute_script_success_workflow(self) -> None:
5050
script_content = "import time\nimport sys\nprint('Hello from integration test')\nsys.stdout.flush()\ntime.sleep(1)\nprint('Execution complete')"
5151
execution_request = {
5252
"script": script_content,
53-
"python_version": "3.11", # Ensure this version is supported by your setup
53+
"lang": "python",
54+
"lang_version": "3.11",
5455
}
5556

5657
# 1. Execute Script
@@ -87,7 +88,7 @@ async def test_execute_script_success_workflow(self) -> None:
8788
assert "Hello from integration test" in result_data.get("output", "")
8889
assert "Execution complete" in result_data.get("output", "")
8990
assert result_data.get("errors") is None or result_data.get("errors") == ""
90-
assert result_data.get("python_version") == execution_request["python_version"]
91+
assert result_data.get("lang_version") == execution_request["lang_version"]
9192

9293
@pytest.mark.asyncio
9394
async def test_execute_script_with_error(self) -> None:
@@ -137,9 +138,9 @@ async def test_k8s_resource_limits(self) -> None:
137138
assert response.status_code == 200
138139
limits = response.json()
139140
expected_keys = ["cpu_limit", "memory_limit", "cpu_request", "memory_request", "execution_timeout",
140-
"supported_python_versions"]
141+
"supported_runtimes"]
141142
assert all(key in limits for key in expected_keys)
142-
assert isinstance(limits["supported_python_versions"], list)
143+
assert isinstance(limits["supported_runtimes"], dict)
143144

144145
@pytest.mark.asyncio
145146
async def test_execute_endpoint_without_auth(self) -> None:

backend/tests/load/locustfile.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ def execute_script(self) -> None:
4747

4848
execution_data = {
4949
"script": script,
50-
"python_version": random.choice(["3.9", "3.10", "3.11"]),
50+
"lang": "python",
51+
"lang_version": random.choice(["3.9", "3.10", "3.11"]),
5152
}
5253

5354
# Execute script

frontend/src/components/Footer.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
Integr8sCode
1515
</h2>
1616
<p class="text-base text-fg-muted dark:text-dark-fg-muted">
17-
Run Python scripts online with ease and security, powered by Kubernetes.
17+
Run code online with ease and security, powered by Kubernetes.
1818
</p>
1919
</div>
2020

0 commit comments

Comments
 (0)