"""FrameGate video client. Python 3.10+; pip install requests. Credentials are read only from FRAMEGATE_API_KEY, never saved to the state file. Keep one state file per business request. Reuse it after network interruption. """ import argparse import json import os from pathlib import Path import time import uuid import requests parser = argparse.ArgumentParser() parser.add_argument('--payload', required=True, help='UTF-8 JSON video request') parser.add_argument('--state', required=True, help='Persistent state file for one request') parser.add_argument('--wait-seconds', type=int, default=1800) args = parser.parse_args() base = os.environ.get('FRAMEGATE_BASE_URL', 'https://api.myanimforge.com').rstrip('/') key = os.environ['FRAMEGATE_API_KEY'] payload = json.loads(Path(args.payload).read_text(encoding='utf-8-sig')) state_path = Path(args.state) if state_path.exists(): state = json.loads(state_path.read_text(encoding='utf-8')) if state['payload'] != payload or state['base_url'] != base: raise SystemExit('This state file belongs to another request. Do not change its payload.') else: state = {'base_url': base, 'payload': payload, 'idempotency_key': str(uuid.uuid4())} # Exclusive creation prevents accidentally replacing another request state. with state_path.open('x', encoding='utf-8') as f: json.dump(state, f, ensure_ascii=False, indent=2) def save(): temporary = state_path.with_suffix(state_path.suffix + '.tmp') temporary.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding='utf-8') temporary.replace(state_path) session = requests.Session() session.headers['Authorization'] = 'Bearer ' + key def checked(response): if not response.ok: print('HTTP', response.status_code, 'Trace', response.headers.get('X-Request-ID')) print(response.text) raise SystemExit('Stopped. Keep the state file and idempotency key; do not resubmit with a new key.') return response.json() try: if not state.get('task_id'): # This POST is attempted once per run, with the same persistent key after a restart. result = checked(session.post(base + '/v1/videos', json=payload, headers={'Idempotency-Key': state['idempotency_key']}, timeout=(10, 240))) state['task_id'] = result['id'] save() print('Task ID:', state['task_id']) deadline = time.monotonic() + args.wait_seconds while time.monotonic() < deadline: response = session.get(base + '/v1/videos/' + state['task_id'], timeout=(10, 30)) if response.status_code == 429: time.sleep(min(120, int(response.headers.get('Retry-After', '60')))) continue result = checked(response) state['last_result'] = result save() status = result['status'] billing_status = result['billing']['status'] print('Execution:', status, 'Billing:', billing_status) if status == 'succeeded' or billing_status == 'settled': print(json.dumps(result, ensure_ascii=False, indent=2)) if billing_status != 'settled': print('Output is ready; billing needs review. Contact support with the task ID.') break if status in ('failed', 'unknown') or billing_status in ('pending', 'released'): print(json.dumps(result, ensure_ascii=False, indent=2)) print('Stopped. Contact support if billing is pending. Do not create a duplicate job.') break time.sleep(10) else: print('Local waiting ended; the server task is not cancelled. Run the same command to continue querying.') except requests.RequestException as exc: print('Network error:', type(exc).__name__) print('Preserve the state file. Reuse this command; do not switch to a new idempotency key.') raise SystemExit(1) finally: session.close()