"""HostDirect public read-only Python SDK and CLI. Python 3.9+, no dependencies.""" import argparse import json import sys import urllib.request import urllib.error __version__ = '1.0.1' class HostDirectError(Exception): """Request, response or selection failure. No credentials are needed.""" def __init__(self, message, status=None, code=None): super().__init__(message) self.status = status self.code = code class Client: """Fetch public data; all filters run locally, without sending user questions.""" def __init__(self, timeout=20): if not isinstance(timeout,(int,float)) or timeout<=0: raise ValueError('timeout must be positive') self.timeout=timeout def catalog(self): """Read every public provider/page once; unpaged full export, no authentication.""" req=urllib.request.Request('https://hostdirect.nl/api/catalog',headers={'Accept':'application/json','User-Agent':'HostDirect-Python/1.0.1'}) try: with urllib.request.urlopen(req,timeout=self.timeout) as response: raw=response.read(4_000_001) if len(raw)>4_000_000:raise HostDirectError('Response exceeds 4 MB limit') if response.headers.get_content_type()!='application/json':raise HostDirectError('Expected JSON response',response.status) data=json.loads(raw) except urllib.error.HTTPError as exc: try: detail=json.loads(exc.read(65536)) except (ValueError,UnicodeError): detail={} raise HostDirectError(detail.get('message','HTTP '+str(exc.code)),exc.code,detail.get('code')) from exc except (urllib.error.URLError,TimeoutError,OSError,ValueError) as exc: raise HostDirectError('Unable to read public catalogue: '+str(exc)) from exc if not isinstance(data,dict) or not isinstance(data.get('providers'),list) or not isinstance(data.get('pages'),list): raise HostDirectError('Unexpected catalogue structure') return data def search(self, query='', category=None): """All matching profiles with original prices, dates and caveats.""" if not isinstance(query,str):raise ValueError('query must be text') if category not in (None,'webhosting','email','cloudopslag'):raise ValueError('Unsupported category') data=self.catalog() rows=[p for p in data['providers'] if (category is None or p['category']==category) and query.casefold() in (p['name']+' '+p['package']+' '+p['description']).casefold()] return self._result(data,rows) def provider(self, provider_id): """One profile selected by exact public ID.""" return self._select([provider_id]) def compare(self, ids): """Two to six profiles, without ranking or inventing a price calculation.""" if not isinstance(ids,(list,tuple)) or not 2<=len(ids)<=6:raise ValueError('Provide 2 to 6 provider IDs') return self._select(ids) def _select(self,ids): if any(not isinstance(x,str) or not x for x in ids) or len(set(ids))!=len(ids):raise ValueError('Use distinct nonempty provider IDs') data=self.catalog();lookup={p['id']:p for p in data['providers']} if any(x not in lookup for x in ids):raise HostDirectError('Unknown provider ID; use search first',code='unknown_provider') return self._result(data,[lookup[x] for x in ids]) @staticmethod def _result(data,rows): return {'providers':rows,'count':len(rows),'disclosure':data['disclosure'],'limitations':data['limitations']} def main(argv=None): parser=argparse.ArgumentParser(description='HostDirect read-only catalogue CLI. Prices may be historical; preserve status, dates and sources.') parser.add_argument('--version',action='version',version=__version__) sub=parser.add_subparsers(dest='command',required=True) sub.add_parser('catalog',help='All public providers and pages as JSON') search=sub.add_parser('search',help='Filter public packages locally');search.add_argument('--query',default='');search.add_argument('--category',choices=['webhosting','email','cloudopslag']) provider=sub.add_parser('provider',help='One provider profile');provider.add_argument('id') compare=sub.add_parser('compare',help='Two to six profiles');compare.add_argument('ids',nargs='+') args=parser.parse_args(argv);client=Client() try: if args.command=='catalog':data=client.catalog() elif args.command=='search':data=client.search(args.query,args.category) elif args.command=='provider':data=client.provider(args.id) else:data=client.compare(args.ids) print(json.dumps(data,ensure_ascii=False,indent=2));return 0 except (HostDirectError,ValueError) as exc: print(json.dumps({'error':str(exc),'code':getattr(exc,'code',None),'status':getattr(exc,'status',None)}),file=sys.stderr);return 1 if __name__=='__main__':sys.exit(main())