"""TraceVero v2 client. Sanitize configuration files locally before submission.""" import json import re from urllib.request import Request, urlopen, build_opener, HTTPRedirectHandler from urllib.parse import urlencode, quote from urllib.error import HTTPError, URLError from http.client import HTTPException def request_id(headers): value = headers.get('X-Request-ID', '') return value if re.fullmatch(r'[a-fA-F0-9-]{36}', value) else None class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None class TraceVeroError(Exception): def __init__(self, code, status, request_id=None): super().__init__(code) self.code, self.status = code, status self.request_id = request_id class TraceVero: def __init__(self, base="https://api.tracevero.com/v2", timeout=20): self.base, self.timeout = base.rstrip("/"), timeout def request(self, path, body=None, *, method=None, access_key=None): payload = None if body is None else json.dumps(body).encode("utf-8") headers={"Content-Type": "application/json"} if payload is not None else {} if access_key:headers["Authorization"]="Bearer "+access_key req = Request(self.base + path, data=payload, headers=headers, method=method) try: with build_opener(NoRedirect()).open(req, timeout=self.timeout) as response: try: return None if response.status == 204 else json.load(response) except (ValueError, UnicodeError): raise TraceVeroError('invalid_response', response.status, request_id(response.headers)) from None except HTTPError as error: try: code = json.load(error).get("error", {}).get("code", "http_error") except (ValueError, AttributeError): code = "http_error" if not isinstance(code, str) or not re.fullmatch(r'[a-z_]{1,64}', code): code = 'http_error' error.close() raise TraceVeroError(code, error.code, request_id(error.headers)) from None except (URLError, OSError, HTTPException): raise TraceVeroError('network_error', 0) from None def plan_workflow(self, **kwargs): return self.request("/workflows/plan", kwargs) def find_capabilities(self, query="", offset=0): return self.request("/capabilities?" + urlencode({"query": query, "offset": offset})) def get_connection_options(self, entry_id): return self.request("/connections/" + quote(entry_id, safe="/")) def get_verification(self, entry_id): return self.request("/verifications/" + quote(entry_id, safe="/")) def check_compatibility(self, entry_id, client, connection="auto"): return self.request("/compatibility/check", {"id": entry_id, "client": client, "connection": connection}) def explain_permissions(self, **kwargs): return self.request("/permissions/explain", kwargs) def validate_config(self, config, client): return self.request("/config/validate", {"config": config, "client": client}) def get_capability_gaps(self, app): return self.request("/capability-gaps?" + urlencode({"app": app})) def get_change_impact(self, snapshot): return self.request("/changes/impact", {"snapshot": snapshot}) def find_replacement(self, app, capabilities=()): return self.request("/replacements?" + urlencode({"app": app, "capabilities": ",".join(capabilities)})) def snapshot(self, **kwargs): return self.request("/workflows/snapshot", kwargs) def create_project(self, plan, interval_seconds=86400): return self.request('/monitoring/projects', {'plan':plan,'interval_seconds':interval_seconds}) def get_project(self, project_id, access_key): return self.request('/monitoring/projects/'+quote(project_id,safe=''), access_key=access_key) def update_project(self, project_id, access_key, **changes): return self.request('/monitoring/projects/'+quote(project_id,safe=''), changes, method='PATCH', access_key=access_key) def delete_project(self, project_id, access_key): return self.request('/monitoring/projects/'+quote(project_id,safe=''), method='DELETE', access_key=access_key) def submit_measurement(self, project_id, access_key, report): return self.request('/monitoring/projects/'+quote(project_id,safe='')+'/measurements', report, access_key=access_key) def export_project(self, project_id, access_key): return self.request('/monitoring/projects/'+quote(project_id,safe='')+'/export', access_key=access_key) def configure(self, entry_id, client, connection="auto"): return self.request('/configure', {'id':entry_id,'client':client,'connection':connection}) def create_recovery(self, project_id, access_key): return self.request('/monitoring/projects/'+quote(project_id,safe='')+'/recovery', {}, access_key=access_key) def recover_project(self, project_id, recovery_key): return self.request('/monitoring/recover', {'id':project_id,'recovery_key':recovery_key}) def set_notifications(self, project_id, access_key, enabled): return self.request('/monitoring/projects/'+quote(project_id,safe='')+'/notifications', {'enabled':enabled}, method='PUT', access_key=access_key) def get_notifications(self, project_id, access_key): return self.request('/monitoring/projects/'+quote(project_id,safe='')+'/notifications', access_key=access_key)