Files
ClaudeandClaude deafb5a26b refactor: drop fetch_url body cap and modernize datetime usage
- fetch_url: remove the 1MB body cap and the streaming helper. The
  manual redirect loop with allowlist re-check (the SSRF fix) is kept
  intact; the body is now read in full via resp.text. Drop the now-
  meaningless `truncated` field from FetchUrlResult and the tests that
  asserted on it. Switched from httpx.stream() back to httpx.get()
  for the redirect loop — cleaner without the body cap.

- datetime: replace deprecated datetime.utcnow() with
  datetime.now(timezone.utc) in submit.py (3 sites) and
  core/job_writer.py (1 site). Update the stale comment in
  core/pending_store.py that referenced the old call.

- Clean up an unused `from common.config import settings` import in
  submit.py that ruff flagged.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-09 14:35:16 +08:00

77 lines
2.8 KiB
Python

# coding=utf-8
"""
@Time :2026/6/24
@Author :tao.chen
Writes LLM-generated PySpark code to disk so the agent can then call
prepare_submit_job(script_path=...) on the returned path.
Resolution order for the output directory:
1. explicit jobs_dir argument (used in tests)
2. SPARK_EXECUTOR_JOBS_DIR environment variable (operator override)
3. DEFAULT_JOBS_DIR constant (./data/jobs/ — gitignored, persists across
container restarts when ./data is mounted)
"""
import os
import secrets
from datetime import datetime, timezone
from common.config import settings
from common.logging import logger
ENV_JOBS_DIR = "SPARK_EXECUTOR_JOBS_DIR"
def resolve_jobs_dir(jobs_dir: str | None = None) -> str:
"""Pick the effective jobs directory in priority order: arg > settings.jobs_dir.
`settings.jobs_dir` itself is computed in `common/config.py` as:
SPARK_EXECUTOR_JOBS_DIR env var (if set) > <data_dir>/jobs
"""
if jobs_dir is not None:
return jobs_dir
return settings.jobs_dir
def _not_utf8_error(exc: UnicodeEncodeError) -> ValueError:
"""Standard 400 error when the LLM-supplied code string cannot be
encoded as UTF-8 (typically because it contains a lone surrogate
from a broken decode earlier in the pipeline)."""
return ValueError(
f"code is not valid UTF-8 and cannot be written as a .py file. "
f"This usually means the LLM produced a string with a lone "
f"surrogate codepoint (U+D800..U+DFFF) — re-generate the code "
f"as plain Unicode. Underlying error: {exc}"
)
def write_job_file(code: str, jobs_dir: str | None = None) -> str:
"""Write `code` to <jobs_dir>/job_<timestamp>_<rand>.py; return abs path.
Creates the directory if missing. Filename is unique per call (timestamp
down to the second + 3 random bytes) so concurrent agents cannot collide.
Raises ValueError (-> 400) if `code` cannot be encoded as UTF-8 (e.g.
contains a lone surrogate). The check is cheap and runs before any
file I/O, so a bad payload never produces a half-written .py.
"""
try:
code.encode("utf-8")
except UnicodeEncodeError as exc:
logger.warning(f"write_job_file rejected: code is not valid UTF-8: {exc}")
raise _not_utf8_error(exc) from exc
effective_dir = resolve_jobs_dir(jobs_dir)
os.makedirs(effective_dir, exist_ok=True)
stamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
name = f"job_{stamp}_{secrets.token_hex(3)}.py"
path = os.path.join(effective_dir, name)
abs_path = os.path.abspath(path)
logger.debug(
f"write_job_file enter jobs_dir={effective_dir} code_bytes={len(code)}"
)
with open(abs_path, "w", encoding="utf-8") as f:
f.write(code)
logger.info(f"write_job_file ok script_path={abs_path} bytes={len(code)}")
return abs_path