Build and Push to Registry / build (push) Successful in 29s
- Rename DockerService to DockerContainer and add folder attribute - Move proxy-related modules (models, validator) to services.mitmproxy - Update imports and references to new module locations - Refactor Docker utilities to use DockerContainer and improve service discovery - Add manager.py with create_proxy utility stub - Initialize discovered Docker services at app startup and warn if none found
165 lines
4.5 KiB
Python
165 lines
4.5 KiB
Python
import asyncio
|
|
import json
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from typing import Any
|
|
|
|
TARGET_RUFF_CODES = ("F821", "F822", "F823", "E999")
|
|
|
|
|
|
class ProxyCodeValidationError(Exception):
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
*,
|
|
errors: list[dict[str, Any]] | None = None,
|
|
) -> None:
|
|
super().__init__(message)
|
|
self.message = message
|
|
self.errors = errors or []
|
|
|
|
def to_http_detail(self) -> dict[str, Any]:
|
|
return {
|
|
"message": self.message,
|
|
"errors": self.errors,
|
|
}
|
|
|
|
|
|
def _validate_python_syntax(code: str) -> None:
|
|
try:
|
|
compile(code, "<proxy_code>", "exec")
|
|
except SyntaxError as error:
|
|
line = error.lineno if error.lineno is not None else 0
|
|
column = error.offset if error.offset is not None else 0
|
|
detail = {
|
|
"code": "COMPILE",
|
|
"message": error.msg,
|
|
"location": {"row": line, "column": column},
|
|
}
|
|
raise ProxyCodeValidationError(
|
|
"Proxy code has invalid Python syntax",
|
|
errors=[detail],
|
|
) from error
|
|
|
|
|
|
def _get_ruff_executable() -> list[str]:
|
|
ruff_path = shutil.which("ruff")
|
|
if ruff_path:
|
|
return [ruff_path]
|
|
|
|
uv_path = shutil.which("uv")
|
|
if uv_path:
|
|
return [uv_path, "run", "ruff"]
|
|
|
|
raise ProxyCodeValidationError("Unable to run ruff: executable not found")
|
|
|
|
|
|
def _parse_ruff_output(output: str) -> list[dict[str, Any]]:
|
|
if not output.strip():
|
|
return []
|
|
|
|
try:
|
|
parsed = json.loads(output)
|
|
except json.JSONDecodeError as error:
|
|
raise ProxyCodeValidationError("Unable to parse ruff JSON output") from error
|
|
|
|
if not isinstance(parsed, list):
|
|
raise ProxyCodeValidationError("Unexpected ruff output format")
|
|
|
|
errors: list[dict[str, Any]] = []
|
|
for item in parsed:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
code = item.get("code")
|
|
if code not in TARGET_RUFF_CODES:
|
|
continue
|
|
|
|
message = item.get("message")
|
|
raw_location = item.get("location")
|
|
location = raw_location if isinstance(raw_location, dict) else {}
|
|
row = location.get("row", 0)
|
|
column = location.get("column", 0)
|
|
|
|
errors.append(
|
|
{
|
|
"code": code,
|
|
"message": str(message) if message is not None else "",
|
|
"location": {"row": row, "column": column},
|
|
}
|
|
)
|
|
|
|
return errors
|
|
|
|
|
|
async def _run_ruff_check(code: str) -> list[dict[str, Any]]:
|
|
command = _get_ruff_executable()
|
|
|
|
file_path = ""
|
|
try:
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w",
|
|
suffix=".py",
|
|
encoding="utf-8",
|
|
delete=False,
|
|
) as handle:
|
|
handle.write(code)
|
|
file_path = handle.name
|
|
|
|
process = await asyncio.create_subprocess_exec(
|
|
*command,
|
|
"check",
|
|
"--output-format=json",
|
|
"--select",
|
|
",".join(TARGET_RUFF_CODES),
|
|
file_path,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
)
|
|
stdout, stderr = await process.communicate()
|
|
finally:
|
|
if file_path:
|
|
try:
|
|
os.unlink(file_path)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
stdout_text = stdout.decode("utf-8", errors="replace")
|
|
stderr_text = stderr.decode("utf-8", errors="replace")
|
|
|
|
if process.returncode not in (0, 1):
|
|
raise ProxyCodeValidationError(
|
|
"Ruff check failed to execute",
|
|
errors=[
|
|
{
|
|
"code": "RUFF",
|
|
"message": stderr_text.strip() or "Unknown ruff failure",
|
|
"location": {"row": 0, "column": 0},
|
|
}
|
|
],
|
|
)
|
|
|
|
if stderr_text.strip() and process.returncode == 0:
|
|
raise ProxyCodeValidationError(
|
|
"Ruff emitted unexpected stderr output",
|
|
errors=[
|
|
{
|
|
"code": "RUFF",
|
|
"message": stderr_text.strip(),
|
|
"location": {"row": 0, "column": 0},
|
|
}
|
|
],
|
|
)
|
|
|
|
return _parse_ruff_output(stdout_text)
|
|
|
|
|
|
async def validate_proxy_code(code: str) -> None:
|
|
_validate_python_syntax(code)
|
|
errors = await _run_ruff_check(code)
|
|
if errors:
|
|
raise ProxyCodeValidationError(
|
|
"Proxy code contains ruff violations",
|
|
errors=errors,
|
|
)
|