jev_client.mojo (3776B)
1 from flare.http import HttpClient 2 from flare.tls import TlsConfig, TlsVerify 3 from json import Value, dumps 4 from hyf_provider.jev_failures import map_jev_failure 5 from hyf_provider.jev_retry import RetryPolicy, should_retry 6 7 8 @fieldwise_init 9 struct JevHttpOutcome(Copyable, Movable): 10 var status: Int 11 var body_text: String 12 13 14 def _is_loopback_host(host: String) -> Bool: 15 return ( 16 host == "localhost" 17 or host == "127.0.0.1" 18 or host == "[::1]" 19 or host == "::1" 20 ) 21 22 23 def validate_jev_base_url(base_url: String) raises -> String: 24 var trimmed = String(String(base_url).strip()) 25 while trimmed.byte_length() > 1 and trimmed.endswith("/"): 26 trimmed = String(trimmed[byte = 0 : trimmed.byte_length() - 1]) 27 if trimmed == "": 28 raise Error("jev base_url must not be empty") 29 var scheme = "" 30 var rest = trimmed 31 if trimmed.startswith("https://"): 32 scheme = "https" 33 rest = String(trimmed[byte=8:]) 34 elif trimmed.startswith("http://"): 35 scheme = "http" 36 rest = String(trimmed[byte=7:]) 37 else: 38 raise Error("jev base_url must use https") 39 if rest.find("@") >= 0: 40 raise Error("jev base_url must not contain credentials") 41 if rest.find("?") >= 0: 42 raise Error("jev base_url must not contain a query") 43 if rest.find("#") >= 0: 44 raise Error("jev base_url must not contain a fragment") 45 var slash = rest.find("/") 46 var host_port = rest if slash < 0 else String(rest[byte=0:slash]) 47 var host = host_port 48 var colon = host_port.find(":") 49 if colon >= 0: 50 if host_port.startswith("["): 51 var close = host_port.find("]") 52 if close >= 0: 53 host = String(host_port[byte = 0 : close + 1]) 54 else: 55 host = String(host_port[byte=0:colon]) 56 if host == "": 57 raise Error("jev base_url must include a host") 58 if scheme == "http" and not _is_loopback_host(host): 59 raise Error("jev base_url must use https unless the host is loopback") 60 return trimmed^ 61 62 63 def jev_systemone_url(base_url: String) raises -> String: 64 return validate_jev_base_url(base_url) + "/v1/systemone" 65 66 67 def post_jev_systemone( 68 base_url: String, body: Value, timeout_ms: Int 69 ) raises -> JevHttpOutcome: 70 var url = jev_systemone_url(base_url) 71 with HttpClient(timeout_ms=timeout_ms, max_redirects=0) as client: 72 var response = client.post(url, dumps(body)) 73 return JevHttpOutcome(status=response.status, body_text=response.text()) 74 75 76 def production_tls_config() -> TlsConfig: 77 return TlsConfig() 78 79 80 def assert_tls_verification_required(config: TlsConfig) raises: 81 if config.verify != TlsVerify.REQUIRED: 82 raise Error("external provider transport must verify TLS certificates") 83 84 85 def redirects_forward_credentials() -> Bool: 86 return False 87 88 89 def failure_kind_for_status(status: Int) -> String: 90 if status == 401: 91 return "authentication" 92 if status == 400 or status == 422 or status == 404: 93 return "validation" 94 if status == 408: 95 return "request_timeout" 96 if status == 429: 97 return "rate_limit" 98 if status == 529: 99 return "overloaded" 100 if status == 500 or status == 502 or status == 503 or status == 504: 101 return "internal_server" 102 return "unexpected_status" 103 104 105 def retry_decision( 106 policy: RetryPolicy, attempt: Int, elapsed_ms: Int, status: Int 107 ) raises -> Bool: 108 if status >= 200 and status < 300: 109 raise Error("retry_decision requires a failure status") 110 var kind = failure_kind_for_status(status) 111 var failure = map_jev_failure( 112 kind if kind != "unexpected_status" else "transport" 113 ) 114 return should_retry(policy, attempt, elapsed_ms, failure.retryable)