"""Pairoo Connect — stdlib client. Keep PAIROO_PROJECT_KEY in a secret manager. Example: client = Pairoo(os.environ['PAIROO_BASE_URL'], os.environ['PAIROO_PROJECT_KEY'], os.environ['PAIROO_PROJECT_ID']) print(client.project()['name']) # A deliberate new request; keep request_key to retry safely on network loss: job = client.generate('Make the approved character more playful', request_key) result = client.wait(job['job_id']) """ from __future__ import annotations import json import re import time import urllib.error import urllib.request from urllib.parse import urlsplit class ConnectError(Exception): def __init__(self,status,code,message): super().__init__(message) self.status,self.code=status,code class Pairoo: def __init__(self,base_url,key,project_id,timeout=20): u=urlsplit(base_url) if u.scheme!='https' and not (u.scheme=='http' and u.hostname in {'localhost','127.0.0.1'}): raise ValueError('Use HTTPS, except for loopback development') if u.username or u.password or u.query or u.fragment: raise ValueError('Use a plain API base URL') if not re.fullmatch(r'[a-f0-9]{24}',project_id): raise ValueError('Invalid project id') self.base=base_url.rstrip('/')+'/v1/projects/'+project_id self.key,self.timeout=key,timeout def request(self,suffix='',body=None,request_key=None): headers={'Accept':'application/json','Authorization':'Bearer '+self.key} if body is not None: if not request_key: raise ValueError('Supply a stable request_key for every mutation') headers.update({'Content-Type':'application/json','Idempotency-Key':request_key}) req=urllib.request.Request(self.base+suffix,data=None if body is None else json.dumps(body,allow_nan=False).encode(),headers=headers) try: with urllib.request.urlopen(req,timeout=self.timeout) as r: return json.load(r) except urllib.error.HTTPError as e: try: error=json.load(e).get('error',{}) except Exception: error={} raise ConnectError(e.code,error.get('code','http_error'),error.get('message','Pairoo request failed')) from None def project(self): return self.request() def generate(self,message,request_key): return self.request('/generate',{'message':message},request_key) def chat(self,message,request_key): return self.request('/chat',{'message':message},request_key) def remember(self,name,text,request_key): return self.request('/memory',{'name':name,'text':text},request_key) def activity(self,title,instructions,request_key,deadline=None): return self.request('/activities',{'title':title,'instructions':instructions,'deadline':deadline},request_key) def propose_activity(self,message,request_key): """Requires community; generates a private draft and consumes owner AI quota.""" return self.request('/activities/generate',{'message':message},request_key) @staticmethod def _identifier(value): if not isinstance(value,str) or not re.fullmatch(r'[a-f0-9]{24}',value): raise ValueError('Invalid resource id') return value def job(self,job_id): return self.request('/jobs/'+self._identifier(job_id)) def activity_detail(self,activity_id): return self.request('/activities/'+self._identifier(activity_id)) def results(self,activity_id): return self.request('/activities/'+self._identifier(activity_id)+'/results.json') def wait(self,job_id,timeout=180): until=time.monotonic()+timeout while time.monotonic()