refactor: split submit_sft_via_cml.sh into submit_sft.sh + submit_cml_eval.sh
- submit_sft.sh: pure SFT submission, prints JobID and exits (no embedded watcher) - submit_cml_eval.sh: pure CML eval, reads workflow_id/version from config.yaml - jupyter_runtime write_text: rewrite to use Contents API instead of terminal websocket - SKILL.md: restrict trigger to explicit UI click only - program.md: update §5.2 docs to reflect two-script workflow Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+96
-26
@@ -796,34 +796,104 @@ else:
|
||||
max_output_chars: int,
|
||||
) -> str:
|
||||
target = self.resolve_workspace_path(path)
|
||||
if newline_at_end and content and not content.endswith('\n'):
|
||||
content += '\n'
|
||||
mode = 'a' if append else 'w'
|
||||
code = r'''
|
||||
from pathlib import Path
|
||||
target = Path(PATH)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
content = CONTENT
|
||||
with target.open(MODE, encoding="utf-8") as fh:
|
||||
fh.write(content)
|
||||
print(f"{'appended' if MODE == 'a' else 'wrote'} {target} ({len(content)} chars)")
|
||||
'''
|
||||
script = (
|
||||
code.replace('PATH', python_literal(target))
|
||||
.replace('CONTENT', python_literal(content))
|
||||
.replace('MODE', python_literal(mode))
|
||||
api_path = api_path_for_absolute_path(target)
|
||||
if not api_path:
|
||||
raise JupyterRuntimeError(f'Invalid remote path: {target}')
|
||||
|
||||
parent_api = '/'.join(api_path.split('/')[:-1])
|
||||
if parent_api:
|
||||
self._ensure_remote_dir(parent_api)
|
||||
|
||||
final_content = content
|
||||
if append:
|
||||
final_content = self._read_remote_text_for_append(api_path) + content
|
||||
if newline_at_end and final_content and not final_content.endswith('\n'):
|
||||
final_content += '\n'
|
||||
|
||||
encoded = base64.b64encode(final_content.encode('utf-8')).decode('ascii')
|
||||
url = f'{self.binding.base_url}/api/contents/{quote(api_path, safe="/")}'
|
||||
response = self._session.put(
|
||||
url,
|
||||
json={
|
||||
'type': 'file',
|
||||
'format': 'base64',
|
||||
'content': encoded,
|
||||
},
|
||||
headers={
|
||||
'X-XSRFToken': self._xsrf_token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout=60,
|
||||
)
|
||||
result = self.run_python(
|
||||
code=script,
|
||||
script_path=None,
|
||||
args=[],
|
||||
stdin=None,
|
||||
timeout_seconds=20.0,
|
||||
max_output_chars=max_output_chars,
|
||||
if response.status_code >= 400:
|
||||
raise JupyterRuntimeError(
|
||||
f'Unable to write remote file: HTTP {response.status_code} {response.text[:500]}'
|
||||
)
|
||||
verb = 'appended' if append else 'wrote'
|
||||
return f'{verb} {target} ({len(final_content)} chars)'
|
||||
|
||||
def _ensure_remote_dir(self, api_path: str) -> None:
|
||||
parts = [segment for segment in api_path.split('/') if segment]
|
||||
cumulative = ''
|
||||
for segment in parts:
|
||||
cumulative = f'{cumulative}/{segment}' if cumulative else segment
|
||||
url = f'{self.binding.base_url}/api/contents/{quote(cumulative, safe="/")}'
|
||||
resp = self._session.get(
|
||||
url,
|
||||
params={'content': 0},
|
||||
headers={'X-XSRFToken': self._xsrf_token},
|
||||
timeout=30,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
kind = resp.json().get('type')
|
||||
if kind == 'directory':
|
||||
continue
|
||||
raise JupyterRuntimeError(
|
||||
f'Cannot create directory at {cumulative}: path exists as {kind}'
|
||||
)
|
||||
if resp.status_code != 404:
|
||||
raise JupyterRuntimeError(
|
||||
f'Unable to inspect remote path {cumulative}: HTTP {resp.status_code} {resp.text[:300]}'
|
||||
)
|
||||
create = self._session.put(
|
||||
url,
|
||||
json={'type': 'directory'},
|
||||
headers={
|
||||
'X-XSRFToken': self._xsrf_token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
if create.status_code >= 400:
|
||||
raise JupyterRuntimeError(
|
||||
f'Unable to create remote directory {cumulative}: HTTP {create.status_code} {create.text[:300]}'
|
||||
)
|
||||
|
||||
def _read_remote_text_for_append(self, api_path: str) -> str:
|
||||
url = f'{self.binding.base_url}/api/contents/{quote(api_path, safe="/")}'
|
||||
resp = self._session.get(
|
||||
url,
|
||||
params={'content': 1},
|
||||
headers={'X-XSRFToken': self._xsrf_token},
|
||||
timeout=60,
|
||||
)
|
||||
if result.exit_code != 0:
|
||||
raise JupyterRuntimeError(result.stdout.strip() or 'remote write_file failed')
|
||||
return result.stdout.strip()
|
||||
if resp.status_code == 404:
|
||||
return ''
|
||||
if resp.status_code >= 400:
|
||||
raise JupyterRuntimeError(
|
||||
f'Unable to read existing remote file for append: HTTP {resp.status_code} {resp.text[:500]}'
|
||||
)
|
||||
payload = resp.json()
|
||||
if payload.get('type') != 'file':
|
||||
raise JupyterRuntimeError(
|
||||
f'Cannot append: remote path {api_path} is not a file (type={payload.get("type")})'
|
||||
)
|
||||
existing = payload.get('content', '')
|
||||
if not isinstance(existing, str):
|
||||
return ''
|
||||
if payload.get('format') == 'base64':
|
||||
return base64.b64decode(existing).decode('utf-8', errors='replace')
|
||||
return existing
|
||||
|
||||
def resolve_workspace_path(self, raw_path: str) -> str:
|
||||
value = raw_path.strip() or '.'
|
||||
|
||||
Reference in New Issue
Block a user