#!/usr/bin/env python3 """Standard-library Waveform workflow. Exact saved intents; no automatic mutation retry.""" import hashlib, json, os, sys, time, uuid, urllib.request, urllib.error, urllib.parse operations=json.loads("{\"identity_get\":{\"method\":\"GET\",\"path\":\"/api/v1/identity\",\"keyed\":false,\"binary\":false},\"generations_list\":{\"method\":\"GET\",\"path\":\"/api/v1/generations\",\"keyed\":false,\"binary\":false},\"generation_get\":{\"method\":\"GET\",\"path\":\"/api/v1/generations/:id\",\"keyed\":false,\"binary\":false},\"generation_request_get\":{\"method\":\"GET\",\"path\":\"/api/v1/generation-requests/:requestKey\",\"keyed\":false,\"binary\":false},\"artifact_manifest\":{\"method\":\"GET\",\"path\":\"/api/v1/generations/:id/artifact-manifest\",\"keyed\":false,\"binary\":false},\"source_get\":{\"method\":\"GET\",\"path\":\"/api/v1/generations/:id/source\",\"keyed\":false,\"binary\":false},\"quality_get\":{\"method\":\"GET\",\"path\":\"/api/v1/generations/:id/quality\",\"keyed\":false,\"binary\":false},\"generation_create\":{\"method\":\"POST\",\"path\":\"/api/v1/generations\",\"keyed\":true,\"binary\":false},\"designs_review\":{\"method\":\"POST\",\"path\":\"/api/v1/generations/:id/review-designs\",\"keyed\":true,\"binary\":false},\"design_edit\":{\"method\":\"POST\",\"path\":\"/api/v1/generations/:id/edit-design\",\"keyed\":true,\"binary\":false},\"design_select\":{\"method\":\"POST\",\"path\":\"/api/v1/generations/:id/select-design\",\"keyed\":true,\"binary\":false},\"source_edit\":{\"method\":\"POST\",\"path\":\"/api/v1/generations/:id/edit\",\"keyed\":true,\"binary\":false},\"generation_finalize\":{\"method\":\"POST\",\"path\":\"/api/v1/generations/:id/finalize\",\"keyed\":true,\"binary\":false},\"generation_continue\":{\"method\":\"POST\",\"path\":\"/api/v1/generations/:id/continue\",\"keyed\":true,\"binary\":false},\"generation_cancel\":{\"method\":\"POST\",\"path\":\"/api/v1/generations/:id/cancel\",\"keyed\":false,\"binary\":false},\"hosting_get\":{\"method\":\"GET\",\"path\":\"/api/v1/pages/:pageId/hosting\",\"keyed\":false,\"binary\":false},\"page_publish\":{\"method\":\"POST\",\"path\":\"/api/v1/generations/:id/publish\",\"keyed\":false,\"binary\":false},\"page_unpublish\":{\"method\":\"POST\",\"path\":\"/api/v1/pages/:pageId/unpublish\",\"keyed\":false,\"binary\":false},\"site_task_start\":{\"method\":\"POST\",\"path\":\"/api/v1/site-tasks\",\"keyed\":true,\"binary\":false},\"site_task_get\":{\"method\":\"GET\",\"path\":\"/api/v1/site-tasks/:id\",\"keyed\":false,\"binary\":false},\"site_tasks_list\":{\"method\":\"GET\",\"path\":\"/api/v1/pages/:pageId/site-tasks\",\"keyed\":false,\"binary\":false},\"site_task_cancel\":{\"method\":\"POST\",\"path\":\"/api/v1/site-tasks/:id/cancel\",\"keyed\":true,\"binary\":false},\"site_for_page\":{\"method\":\"GET\",\"path\":\"/api/v1/pages/:pageId/site\",\"keyed\":false,\"binary\":false},\"site_create\":{\"method\":\"POST\",\"path\":\"/api/v1/sites\",\"keyed\":true,\"binary\":false},\"site_get\":{\"method\":\"GET\",\"path\":\"/api/v1/sites/:id\",\"keyed\":false,\"binary\":false},\"site_history\":{\"method\":\"GET\",\"path\":\"/api/v1/sites/:id/history\",\"keyed\":false,\"binary\":false},\"site_edit\":{\"method\":\"PUT\",\"path\":\"/api/v1/sites/:id/scope\",\"keyed\":true,\"binary\":false},\"site_facts\":{\"method\":\"POST\",\"path\":\"/api/v1/sites/:id/facts\",\"keyed\":true,\"binary\":false},\"site_approve\":{\"method\":\"POST\",\"path\":\"/api/v1/sites/:id/approve\",\"keyed\":true,\"binary\":false},\"site_propose\":{\"method\":\"POST\",\"path\":\"/api/v1/sites/:id/proposals\",\"keyed\":true,\"binary\":false},\"site_proposals\":{\"method\":\"GET\",\"path\":\"/api/v1/sites/:id/proposals\",\"keyed\":false,\"binary\":false},\"site_job_get\":{\"method\":\"GET\",\"path\":\"/api/v1/site-jobs/:id\",\"keyed\":false,\"binary\":false},\"site_job_cancel\":{\"method\":\"POST\",\"path\":\"/api/v1/site-jobs/:id/cancel\",\"keyed\":true,\"binary\":false},\"site_proposal_apply\":{\"method\":\"POST\",\"path\":\"/api/v1/site-jobs/:id/apply\",\"keyed\":true,\"binary\":false},\"site_posts\":{\"method\":\"GET\",\"path\":\"/api/v1/sites/:id/posts\",\"keyed\":false,\"binary\":false},\"site_post_get\":{\"method\":\"GET\",\"path\":\"/api/v1/sites/:id/posts/:postId\",\"keyed\":false,\"binary\":false},\"site_post_history\":{\"method\":\"GET\",\"path\":\"/api/v1/sites/:id/posts/:postId/history\",\"keyed\":false,\"binary\":false},\"site_post_create\":{\"method\":\"POST\",\"path\":\"/api/v1/sites/:id/posts\",\"keyed\":true,\"binary\":false},\"site_post_edit\":{\"method\":\"PUT\",\"path\":\"/api/v1/sites/:id/posts/:postId\",\"keyed\":true,\"binary\":false},\"site_posts_select\":{\"method\":\"POST\",\"path\":\"/api/v1/sites/:id/blog-selection\",\"keyed\":true,\"binary\":false},\"site_build\":{\"method\":\"POST\",\"path\":\"/api/v1/sites/:id/builds\",\"keyed\":true,\"binary\":false},\"site_revision_options\":{\"method\":\"GET\",\"path\":\"/api/v1/sites/:id/revision-options\",\"keyed\":false,\"binary\":false},\"site_revise\":{\"method\":\"POST\",\"path\":\"/api/v1/sites/:id/revisions\",\"keyed\":true,\"binary\":false},\"site_builds\":{\"method\":\"GET\",\"path\":\"/api/v1/sites/:id/builds\",\"keyed\":false,\"binary\":false},\"site_build_get\":{\"method\":\"GET\",\"path\":\"/api/v1/site-builds/:id\",\"keyed\":false,\"binary\":false},\"billing_summary\":{\"method\":\"GET\",\"path\":\"/api/v1/billing/summary\",\"keyed\":false,\"binary\":false},\"billing_receipt\":{\"method\":\"GET\",\"path\":\"/api/v1/billing/usage/:id\",\"keyed\":false,\"binary\":false},\"billing_quote\":{\"method\":\"POST\",\"path\":\"/api/v1/billing/quote\",\"keyed\":false,\"binary\":false},\"site_release_options\":{\"method\":\"GET\",\"path\":\"/api/v1/sites/:id/release-options\",\"keyed\":false,\"binary\":false},\"site_releases\":{\"method\":\"GET\",\"path\":\"/api/v1/sites/:id/releases\",\"keyed\":false,\"binary\":false},\"site_release_create\":{\"method\":\"POST\",\"path\":\"/api/v1/sites/:id/releases\",\"keyed\":true,\"binary\":false},\"site_release_get\":{\"method\":\"GET\",\"path\":\"/api/v1/sites/:id/releases/:releaseId\",\"keyed\":false,\"binary\":false},\"site_release_download\":{\"method\":\"GET\",\"path\":\"/api/v1/sites/:id/releases/:releaseId/download\",\"keyed\":false,\"binary\":true},\"site_publication_get\":{\"method\":\"GET\",\"path\":\"/api/v1/sites/:id/publication\",\"keyed\":false,\"binary\":false},\"site_publish\":{\"method\":\"POST\",\"path\":\"/api/v1/sites/:id/publication\",\"keyed\":true,\"binary\":false},\"webhooks_list\":{\"method\":\"GET\",\"path\":\"/api/v1/integrations/webhooks\",\"keyed\":false,\"binary\":false},\"webhook_get\":{\"method\":\"GET\",\"path\":\"/api/v1/integrations/webhooks/:id\",\"keyed\":false,\"binary\":false},\"webhook_create\":{\"method\":\"POST\",\"path\":\"/api/v1/integrations/webhooks\",\"keyed\":true,\"binary\":false},\"webhook_update\":{\"method\":\"PUT\",\"path\":\"/api/v1/integrations/webhooks/:id\",\"keyed\":false,\"binary\":false},\"webhook_deliveries\":{\"method\":\"GET\",\"path\":\"/api/v1/integrations/webhooks/:id/deliveries\",\"keyed\":false,\"binary\":false},\"webhook_retry\":{\"method\":\"POST\",\"path\":\"/api/v1/integrations/webhooks/:id/deliveries/:deliveryId/retry\",\"keyed\":false,\"binary\":false},\"domain_connect\":{\"method\":\"POST\",\"path\":\"/api/v1/pages/:pageId/domains\",\"keyed\":false,\"binary\":false},\"domain_check\":{\"method\":\"POST\",\"path\":\"/api/v1/pages/:pageId/domains/:domainId/check\",\"keyed\":false,\"binary\":false},\"domain_disconnect\":{\"method\":\"DELETE\",\"path\":\"/api/v1/pages/:pageId/domains/:domainId\",\"keyed\":false,\"binary\":false},\"forms_list\":{\"method\":\"GET\",\"path\":\"/api/v1/pages/:pageId/forms\",\"keyed\":false,\"binary\":false},\"form_create\":{\"method\":\"POST\",\"path\":\"/api/v1/pages/:pageId/forms\",\"keyed\":false,\"binary\":false},\"form_update\":{\"method\":\"PUT\",\"path\":\"/api/v1/pages/:pageId/forms/:formId\",\"keyed\":false,\"binary\":false},\"form_copy\":{\"method\":\"POST\",\"path\":\"/api/v1/pages/:pageId/forms/:formId/copy\",\"keyed\":false,\"binary\":false},\"leads_list\":{\"method\":\"GET\",\"path\":\"/api/v1/pages/:pageId/forms/:formId/leads\",\"keyed\":false,\"binary\":false},\"form_test\":{\"method\":\"POST\",\"path\":\"/api/v1/pages/:pageId/forms/:formId/test\",\"keyed\":true,\"binary\":false},\"lead_retry\":{\"method\":\"POST\",\"path\":\"/api/v1/pages/:pageId/forms/:formId/leads/:leadId/retry\",\"keyed\":false,\"binary\":false},\"reference_read\":{\"method\":\"GET\",\"path\":\"/api/v1/generations/:id/reference\",\"keyed\":false,\"binary\":true},\"design_image_read\":{\"method\":\"GET\",\"path\":\"/api/v1/generations/:id/designs/:candidateId/image\",\"keyed\":false,\"binary\":true},\"brand_logo_read\":{\"method\":\"GET\",\"path\":\"/api/v1/generations/:id/brand/logo\",\"keyed\":false,\"binary\":true},\"download_read\":{\"method\":\"GET\",\"path\":\"/api/v1/generations/:id/download\",\"keyed\":false,\"binary\":true}}") origin=os.environ.get('WAVEFORM_ORIGIN','').rstrip('/') try: url=urllib.parse.urlsplit(origin) except ValueError: raise SystemExit('Use the trusted HTTPS studio origin (HTTP loopback is allowed locally).') from None if not url.hostname or url.username or url.password or url.path or url.query or url.fragment or not (url.scheme=='https' or url.scheme=='http' and url.hostname in ('localhost','127.0.0.1','::1')): raise SystemExit('Use the trusted HTTPS studio origin (HTTP loopback is allowed locally).') token=os.environ.get('WAVEFORM_API_KEY') if not token: raise SystemExit('Set WAVEFORM_API_KEY privately.') class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self,req,fp,code,msg,headers,newurl): return None def call(name,inputs,timeout=30): op=operations[name]; path=op['path'] for key,value in inputs.get('params',{}).items(): path=path.replace(':'+key,urllib.parse.quote(str(value),safe='')) if inputs.get('query'): path+='?'+urllib.parse.urlencode(inputs['query']) headers={'Authorization':'Bearer '+token} if inputs.get('idempotencyKey'): headers['Idempotency-Key']=inputs['idempotencyKey'] body=None if 'body' in inputs: body=json.dumps(inputs['body'],ensure_ascii=False,separators=(',',':')).encode(); headers['Content-Type']='application/json' req=urllib.request.Request(origin+path,data=body,headers=headers,method=op['method']) with urllib.request.build_opener(NoRedirect).open(req,timeout=max(.001,timeout)) as response: if response.status==204: return None return response.read() if op['binary'] else json.load(response) def load(file): with open(file,encoding='utf8') as stream: return json.load(stream) def fingerprint(value): return hashlib.sha256(json.dumps([value['origin'],value['workspaceId'],value['operation'],value['input']],ensure_ascii=False,separators=(',',':')).encode()).hexdigest() def save(file,value,fresh=False): target=file if fresh else file+'.'+str(uuid.uuid4())+'.tmp' try: fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600) with os.fdopen(fd,'w',encoding='utf8') as stream: json.dump(value,stream,ensure_ascii=False,indent=2) if not fresh: os.replace(target,file) finally: if not fresh and os.path.exists(target): os.unlink(target) def main(args): if not args: raise ValueError('Use prepare REQUEST.json INTENT.json | send/recover INTENT.json | read REQUEST.json | status/watch/cancel GENERATION_ID | download GENERATION_ID OUTPUT.zip') command=args[0] if command=='prepare': request=load(args[1]); op=operations.get(request['operation']) if not op or op['method']=='GET': raise ValueError('Choose one installed bearer mutation.') inputs=request['input'] if op['keyed'] and not inputs.get('idempotencyKey'): inputs['idempotencyKey']=str(uuid.uuid4()) identity=call('identity_get',{}) intent=dict(version=1,driver='python',origin=origin,workspaceId=identity['workspaceId'],operation=request['operation'],input=inputs,attempted=False) intent['fingerprint']=fingerprint(intent); save(args[2],intent,True) print('Prepared '+args[2]+' for workspace '+identity['workspaceId']+'. No mutation sent.') elif command in ('send','recover'): file=args[1]; lock=os.open(file+'.lock',os.O_CREAT|os.O_EXCL|os.O_WRONLY,0o600) try: intent=load(file); op=operations.get(intent['operation']) if intent.get('version')!=1 or intent.get('driver')!='python' or intent['origin']!=origin or fingerprint(intent)!=intent['fingerprint'] or not op: raise ValueError('Intent changed or belongs to another origin/runner.') if call('identity_get',{})['workspaceId']!=intent['workspaceId']: raise ValueError('Credential workspace differs from saved intent.') if 'result' in intent: print(json.dumps(intent['result'])); return if intent['attempted'] and (command!='recover' or not op['keyed'] or not intent['input'].get('idempotencyKey')): raise ValueError('Acceptance is uncertain. Read saved state; only the identical keyed intent can use recover.') intent['attempted']=True; save(file,intent) result=call(intent['operation'],intent['input']); intent['result']=result; save(file,intent); print(json.dumps(result)) finally: os.close(lock); os.unlink(file+'.lock') elif command=='read': request=load(args[1]); op=operations.get(request['operation']) if not op or op['method']!='GET' or op['binary']: raise ValueError('read accepts only an installed JSON GET operation.') print(json.dumps(call(request['operation'],request['input']))) elif command in ('status','watch'): deadline=time.monotonic()+300 while True: result=call('generation_get',{'params':{'id':args[1]}},min(30,max(.001,deadline-time.monotonic()))); print(json.dumps(result)) if command=='status' or result['state'] not in ('queued','running'): break if time.monotonic()>=deadline: print('Local wait ended; saved job continues.',file=sys.stderr); break time.sleep(min(3,max(0,deadline-time.monotonic()))) if time.monotonic()>=deadline: print('Local wait ended; saved job continues.',file=sys.stderr); break elif command=='cancel': print(json.dumps(call('generation_cancel',{'params':{'id':args[1]}}))) elif command=='download': data=call('download_read',{'params':{'id':args[1]}}) with os.fdopen(os.open(args[2],os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600),'wb') as stream: stream.write(data) print('Saved '+args[2]) else: raise ValueError('Unknown command.') try: main(sys.argv[1:]) except urllib.error.HTTPError as error: print('HTTP '+str(error.code)+': '+error.read().decode('utf8',errors='replace'),file=sys.stderr); sys.exit(1) except Exception as error: print(str(error),file=sys.stderr); sys.exit(1)