def test_generic_exception(): headers = {"Content-Type": "application/cloudevents+json"} data = json.dumps({ "specversion": "1.0", "source": "s", "type": "t", "id": "1234-1234-1234", "data": "", }) with pytest.raises(cloud_exceptions.GenericException) as e: from_http({}, None) e.errisinstance(cloud_exceptions.MissingRequiredFields) with pytest.raises(cloud_exceptions.GenericException) as e: from_http({}, 123) e.errisinstance(cloud_exceptions.InvalidStructuredJSON) with pytest.raises(cloud_exceptions.GenericException) as e: from_http(headers, data, data_unmarshaller=lambda x: 1 / 0) e.errisinstance(cloud_exceptions.DataUnmarshallerError) with pytest.raises(cloud_exceptions.GenericException) as e: event = from_http(headers, data) to_binary(event, data_marshaller=lambda x: 1 / 0) e.errisinstance(cloud_exceptions.DataMarshallerError)
def test_none_data_cloudevent(specversion): event = CloudEvent({ "source": "<my-url>", "type": "issue.example", "specversion": specversion, }) to_binary(event) to_structured(event)
async def test_predict_ce_avro_binary(self, http_server_client): schema = avro.schema.parse(test_avsc_schema) msg = {"name": "foo", "favorite_number": 1, "favorite_color": "pink"} writer = avro.io.DatumWriter(schema) bytes_writer = io.BytesIO() encoder = avro.io.BinaryEncoder(bytes_writer) writer.write(msg, encoder) data = bytes_writer.getvalue() event = dummy_cloud_event(data, set_contenttype=True) # Creates the HTTP request representation of the CloudEvent in binary content mode headers, body = to_binary(event) resp = await http_server_client.fetch('/v1/models/TestModel:predict', method="POST", headers=headers, body=body) assert resp.code == 200 assert resp.headers['content-type'] == "application/json" assert resp.headers['ce-specversion'] == "1.0" assert resp.headers["ce-id"] != "36077800-0c23-4f38-a0b4-01f4369f670a" assert resp.headers['ce-source'] == "io.kserve.kfserver.TestModel" assert resp.headers['ce-type'] == "io.kserve.inference.response" assert resp.headers['ce-time'] > "2021-01-28T21:04:43.144141+00:00" assert resp.body == b'{"predictions": [["foo", 1, "pink"]]}'
def test_create_binary_image(): # Create image and turn image into bytes attributes = { "type": "com.example.string", "source": "https://example.com/event-producer", } # Create CloudEvent event = CloudEvent(attributes, image_bytes) # Create http headers/body content headers, body = to_binary(event) # Unmarshall CloudEvent and re-create image reconstruct_event = from_http(headers, body, data_unmarshaller=lambda x: io.BytesIO(x)) # reconstruct_event.data is an io.BytesIO object due to data_unmarshaller restore_image = Image.open(reconstruct_event.data) assert restore_image.size == image_expected_shape # # Test cloudevent extension from http fields and data assert isinstance(body, bytes) assert body == image_bytes
def create_response_cloudevent(model_name: str, body: Union[Dict, CloudEvent], response: Dict, binary_event=False) -> tuple: ce_attributes = {} if os.getenv("CE_MERGE", "false").lower() == "true": if binary_event: ce_attributes = body._attributes if "datacontenttype" in ce_attributes: # Optional field so must check del ce_attributes["datacontenttype"] else: ce_attributes = body del ce_attributes["data"] # Remove these fields so we generate new ones del ce_attributes["id"] del ce_attributes["time"] ce_attributes["type"] = os.getenv("CE_TYPE", "io.kserve.inference.response") ce_attributes["source"] = os.getenv("CE_SOURCE", f"io.kserve.kfserver.{model_name}") event = CloudEvent(ce_attributes, response) if binary_event: event_headers, event_body = to_binary(event) else: event_headers, event_body = to_structured(event) return event_headers, event_body
def test_http_data_marshaller_exception(binary_headers, structured_data): # binary event = from_http(binary_headers, None) with pytest.raises(cloud_exceptions.DataMarshallerError) as e: to_binary(event, data_marshaller=lambda x: 1 / 0) assert ("Failed to marshall data with error: " "ZeroDivisionError('division by zero')" in str(e.value)) # structured headers = {"Content-Type": "application/cloudevents+json"} event = from_http(headers, structured_data) with pytest.raises(cloud_exceptions.DataMarshallerError) as e: to_structured(event, data_marshaller=lambda x: 1 / 0) assert ("Failed to marshall data with error: " "ZeroDivisionError('division by zero')" in str(e.value))
async def test_predict_merge_binary_ce_attributes(self, http_server_client): with mock.patch.dict(os.environ, {"CE_MERGE": "true"}): event = dummy_cloud_event({"instances": [[1, 2]]}, set_contenttype=True, add_extension=True) headers, body = to_binary(event) resp = await http_server_client.fetch( '/v1/models/TestModel:predict', method="POST", headers=headers, body=body) assert resp.code == 200 assert resp.headers['content-type'] == "application/json" assert resp.headers['ce-specversion'] == "1.0" assert resp.headers[ "ce-id"] != "36077800-0c23-4f38-a0b4-01f4369f670a" # Added by add_extension=True in dummy_cloud_event assert resp.headers['ce-custom-extension'] == 'custom-value' assert resp.headers['ce-source'] == "io.kserve.kfserver.TestModel" assert resp.headers['ce-type'] == "io.kserve.inference.response" assert resp.headers['ce-time'] > "2021-01-28T21:04:43.144141+00:00" assert resp.body == b'{"predictions": [[1, 2]]}'
async def test_predict_ce_bytes_bad_format_exception( self, http_server_client): event = dummy_cloud_event(b'{', set_contenttype=True) headers, body = to_binary(event) with pytest.raises( HTTPClientError, match=r".*HTTP 400: Unrecognized request format: " r"Expecting property name enclosed in double quotes.*"): await http_server_client.fetch('/v1/models/TestModel:predict', method="POST", headers=headers, body=body)
async def post(self, name: str): if has_binary_headers(self.request.headers): try: #Use default unmarshaller if contenttype is set in header if "ce-contenttype" in self.request.headers: body = from_http(self.request.headers, self.request.body) else: body = from_http(self.request.headers, self.request.body, lambda x: x) except (ce.MissingRequiredFields, ce.InvalidRequiredFields, ce.InvalidStructuredJSON, ce.InvalidHeadersFormat, ce.DataMarshallerError, ce.DataUnmarshallerError) as e: raise tornado.web.HTTPError( status_code=HTTPStatus.BAD_REQUEST, reason="Cloud Event Exceptions: %s" % e) else: try: body = json.loads(self.request.body) except json.decoder.JSONDecodeError as e: raise tornado.web.HTTPError( status_code=HTTPStatus.BAD_REQUEST, reason="Unrecognized request format: %s" % e) model = self.get_model(name) request = model.preprocess(body) request = self.validate(request) response = (await model.predict(request)) if inspect.iscoroutinefunction( model.predict) else model.predict(request) response = model.postprocess(response) if has_binary_headers(self.request.headers): event = CloudEvent(body._attributes, response) if is_binary(self.request.headers): eventheader, eventbody = to_binary(event) elif is_structured(self.request.headers): eventheader, eventbody = to_structured(event) for k, v in eventheader.items(): if k != "ce-time": self.set_header(k, v) else: #utc now() timestamp self.set_header( 'ce-time', datetime.utcnow().replace(tzinfo=pytz.utc).strftime( '%Y-%m-%dT%H:%M:%S.%f%z')) if isinstance(eventbody, (bytes, bytearray)): response = eventbody else: response = eventbody.data self.write(response)
def test_binary_request(client): # This data defines a binary cloudevent attributes = { "type": "com.example.sampletype1", "source": "https://example.com/event-producer", } data = {"message": "Hello World!"} event = CloudEvent(attributes, data) headers, body = to_binary(event) r = client.post("/", headers=headers, data=body) assert r.status_code == 204
def test_post_cloud_event(client): """ Test that we can successfully receive a CloudEvent """ attributes = { "type": "com.test.parliament", "source": "https://test.com/parliament", } data = {"message": "Hello World!"} event = CloudEvent(attributes, data) headers, body = to_binary(event) rv = client.post("/", data=body, headers=headers) assert rv.data == b"Hello World!"
async def test_predict_ce_bytes_bad_hex_format_exception( self, http_server_client): event = dummy_cloud_event(b'0\x80\x80\x06World!\x00\x00', set_contenttype=True) headers, body = to_binary(event) with pytest.raises( HTTPClientError, match=r".*HTTP 400: Unrecognized request format: " r"'utf-8' codec can't decode byte 0x80 in position 1: invalid start byte.*" ): await http_server_client.fetch('/v1/models/TestModel:predict', method="POST", headers=headers, body=body)
def send_binary_cloud_event(url): # This data defines a binary cloudevent attributes = { "type": "com.example.sampletype1", "source": "https://example.com/event-producer", } data = {"message": "Hello World!"} event = CloudEvent(attributes, data) headers, body = to_binary(event) # send and print event requests.post(url, headers=headers, data=body) print(f"Sent {event['id']} from {event['source']} with {event.data}")
def test_uppercase_headers_with_none_data_binary(): headers = { "Ce-Id": "my-id", "Ce-Source": "<event-source>", "Ce-Type": "cloudevent.event.type", "Ce-Specversion": "1.0", } event = from_http(headers, None) for key in headers: assert event[key.lower()[3:]] == headers[key] assert event.data == None _, new_data = to_binary(event) assert new_data == None
def send_binary_cloud_event(url: str): # Create cloudevent attributes = { "type": "com.example.string", "source": "https://example.com/event-producer", } event = CloudEvent(attributes, image_bytes) # Create cloudevent HTTP headers and content headers, body = to_binary(event) # Send cloudevent requests.post(url, headers=headers, data=body) print(f"Sent {event['id']} of type {event['type']}")
async def test_predict_ce_bytes_bad_format_exception( self, http_server_client): event = dummy_cloud_event(b'{', set_contenttype=True) headers, body = to_binary(event) with pytest.raises(HTTPClientError) as err: _ = await http_server_client.fetch('/v1/models/TestModel:predict', method="POST", headers=headers, body=body) assert err.value.code == 400 error_regex = re.compile( "Failed to decode or parse binary json cloudevent: " "Expecting property name enclosed in double quotes.*") response = json.loads(err.value.response.body) assert error_regex.match(response["error"]) is not None
def test_server_binary(client): # Create cloudevent attributes = { "type": "com.example.string", "source": "https://example.com/event-producer", } event = CloudEvent(attributes, image_bytes) # Create cloudevent HTTP headers and content headers, body = to_binary(event) # Send cloudevent r = client.post("/", headers=headers, data=body) assert r.status_code == 200 assert r.data.decode() == f"Found image of size {image_expected_shape}"
def test_binary_to_request(specversion): attributes = { "specversion": specversion, "type": "word.found.name", "id": "96fb5f0b-001e-0108-6dfe-da6e2806f124", "source": "pytest", } data = {"message": "Hello World!"} event = CloudEvent(attributes, data) headers, body_bytes = to_binary(event) body = json.loads(body_bytes) for key in data: assert body[key] == data[key] for key in attributes: assert attributes[key] == headers["ce-" + key]
async def post(self, name: str): if has_binary_headers(self.request.headers): try: # Use default unmarshaller if contenttype is set in header if "ce-contenttype" in self.request.headers: body = from_http(self.request.headers, self.request.body) else: body = from_http(self.request.headers, self.request.body, lambda x: x) except (ce.MissingRequiredFields, ce.InvalidRequiredFields, ce.InvalidStructuredJSON, ce.InvalidHeadersFormat, ce.DataMarshallerError, ce.DataUnmarshallerError) as e: raise tornado.web.HTTPError( status_code=HTTPStatus.BAD_REQUEST, reason="Cloud Event Exceptions: %s" % e) else: try: body = json.loads(self.request.body) except json.decoder.JSONDecodeError as e: raise tornado.web.HTTPError( status_code=HTTPStatus.BAD_REQUEST, reason="Unrecognized request format: %s" % e) # call model locally or remote model workers model = self.get_model(name) if not isinstance(model, RayServeHandle): response = await model(body) else: model_handle = model response = await model_handle.remote(body) # process response from the model if has_binary_headers(self.request.headers): event = CloudEvent(body._attributes, response) if is_binary(self.request.headers): eventheader, eventbody = to_binary(event) elif is_structured(self.request.headers): eventheader, eventbody = to_structured(event) for k, v in eventheader.items(): if k != "ce-time": self.set_header(k, v) else: # utc now() timestamp self.set_header( 'ce-time', datetime.utcnow().replace(tzinfo=pytz.utc).strftime( '%Y-%m-%dT%H:%M:%S.%f%z')) response = eventbody self.write(response)
async def test_predict_ce_binary_bytes(self, http_server_client): event = dummy_cloud_event(b'{"instances":[[1,2]]}', set_contenttype=True) headers, body = to_binary(event) resp = await http_server_client.fetch('/v1/models/TestModel:predict', method="POST", headers=headers, body=body) assert resp.code == 200 assert resp.headers['content-type'] == "application/json" assert resp.headers['ce-specversion'] == "1.0" assert resp.headers["ce-id"] != "36077800-0c23-4f38-a0b4-01f4369f670a" assert resp.headers['ce-source'] == "io.kserve.kfserver.TestModel" assert resp.headers['ce-type'] == "io.kserve.inference.response" assert resp.headers['ce-time'] > "2021-01-28T21:04:43.144141+00:00" assert resp.body == b'{"predictions": [[1, 2]]}'
async def test_predict_ce_bytes_bad_hex_format_exception( self, http_server_client): event = dummy_cloud_event(b'0\x80\x80\x06World!\x00\x00', set_contenttype=True) headers, body = to_binary(event) with pytest.raises(HTTPClientError) as err: _ = await http_server_client.fetch('/v1/models/TestModel:predict', method="POST", headers=headers, body=body) assert err.value.code == 400 error_regex = re.compile( "Failed to decode or parse binary json cloudevent: " "'utf-8' codec can't decode byte 0x80 in position 1: invalid start byte.*" ) response = json.loads(err.value.response.body) assert error_regex.match(response["error"]) is not None
def test_known_empty_edge_cases(binary_headers, test_data): expect_data = test_data if test_data in ["", b""]: expect_data = None elif test_data == (): # json.dumps(()) outputs '[]' hence list not tuple check expect_data = [] # Remove ce- prefix headers = {key[3:]: value for key, value in binary_headers.items()} # binary event = from_http(*to_binary(CloudEvent(headers, test_data))) assert event.data == expect_data # structured event = from_http(*to_structured(CloudEvent(headers, test_data))) assert event.data == expect_data
async def test_predict_ce_binary_bytes(self, http_server_client): event = dummy_cloud_event(b'{"instances":[[1,2]]}', set_contenttype=True) headers, body = to_binary(event) resp = await http_server_client.fetch('/v1/models/TestModel:predict', method="POST", headers=headers, body=body) assert resp.code == 200 assert resp.body == b'{"predictions": [[1, 2]]}' assert resp.headers[ 'content-type'] == "application/x-www-form-urlencoded" assert resp.headers['ce-specversion'] == "1.0" assert resp.headers['ce-id'] == "36077800-0c23-4f38-a0b4-01f4369f670a" assert resp.headers[ 'ce-source'] == "https://example.com/event-producer" assert resp.headers['ce-type'] == "com.example.sampletype1" assert resp.headers[ 'ce-datacontenttype'] == "application/x-www-form-urlencoded" assert resp.headers['ce-time'] > "2021-01-28T21:04:43.144141+00:00"
def test_roundtrip_non_json_event(converter, specversion): input_data = io.BytesIO() for _ in range(100): for j in range(20): assert 1 == input_data.write(j.to_bytes(1, byteorder="big")) compressed_data = bz2.compress(input_data.getvalue()) attrs = {"source": "test", "type": "t"} event = CloudEvent(attrs, compressed_data) if converter == converters.TypeStructured: headers, data = to_structured(event, data_marshaller=lambda x: x) elif converter == converters.TypeBinary: headers, data = to_binary(event, data_marshaller=lambda x: x) headers["binary-payload"] = "true" # Decoding hint for server _, r = app.test_client.post("/event", headers=headers, data=data) assert r.status_code == 200 for key in attrs: assert r.headers[key] == attrs[key] assert compressed_data == r.body, r.body
def test_wrong_specversion_to_request(): event = CloudEvent({"source": "s", "type": "t"}, None) with pytest.raises(cloud_exceptions.InvalidRequiredFields) as e: event["specversion"] = "0.2" to_binary(event) assert "Unsupported specversion: 0.2" in str(e.value)
def test_to_binary_extensions(specversion): event = CloudEvent(test_attributes, test_data) headers, body = to_binary(event) assert "ce-ext1" in headers assert headers.get("ce-ext1") == test_attributes["ext1"]
def test_binary_event_0_3(client, cloudevent_0_3): headers, data = to_binary(cloudevent_0_3) resp = client.post("/", headers=headers, data=data) assert resp.status_code == 200 assert resp.data == b"OK"
def test_to_binary_http_deprecated(event): with pytest.deprecated_call(): assert to_binary(event) == to_binary_http(event)