def test_date_in_range( self, time_multiplier: int, endpoint: Endpoint, ) -> None: """ If a date header is within five minutes before or after the request is sent, no error is returned. Because there is a small delay in sending requests and Vuforia isn't consistent, some leeway is given. """ url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc skew = { 'vws.vuforia.com': _VWS_MAX_TIME_SKEW, 'cloudreco.vuforia.com': _VWQ_MAX_TIME_SKEW, }[netloc] time_difference_from_now = skew - _LEEWAY time_difference_from_now *= time_multiplier gmt = ZoneInfo('GMT') with freeze_time(datetime.now(tz=gmt) + time_difference_from_now): date = rfc_1123_date() endpoint_headers = dict(endpoint.prepared_request.headers) content = endpoint.prepared_request.body or b'' assert isinstance(content, bytes) authorization_string = authorization_header( access_key=endpoint.access_key, secret_key=endpoint.secret_key, method=str(endpoint.prepared_request.method), content=content, content_type=endpoint.auth_header_content_type, date=date, request_path=endpoint.prepared_request.path_url, ) headers = { **endpoint_headers, 'Authorization': authorization_string, 'Date': date, } endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) session = requests.Session() response = session.send( # type: ignore request=endpoint.prepared_request, ) url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc if netloc == 'cloudreco.vuforia.com': assert_query_success(response=response) return assert_vws_response( response=response, status_code=endpoint.successful_headers_status_code, result_code=endpoint.successful_headers_result_code, )
def test_authorization_header() -> None: """ The authorization header is constructed as described in the Vuforia documentation. This example has been run on known-working code and so any refactor should continue to pass this test. """ access_key = 'my_access_key' secret_key = 'my_secret_key' method = 'HTTPMETHOD' content = b'some_bytes' content_type = 'some/content/type' date = 'some_date_string' request_path = '/foo' result = vws_auth_tools.authorization_header( access_key=access_key, secret_key=secret_key, method=method, content=content, content_type=content_type, date=date, request_path=request_path, ) assert result == 'VWS my_access_key:8Uy6SKuO5sSBY2X8/znlPFmDF/k='
def test_incorrect_date_format( self, endpoint: Endpoint, ) -> None: """ A `BAD_REQUEST` response is returned when the date given in the date header is not in the expected format (RFC 1123) to VWS API. An `UNAUTHORIZED` response is returned to the VWQ API. """ gmt = ZoneInfo('GMT') with freeze_time(datetime.now(tz=gmt)): now = datetime.now() date_incorrect_format = now.strftime('%a %b %d %H:%M:%S') endpoint_headers = dict(endpoint.prepared_request.headers) content = endpoint.prepared_request.body or b'' assert isinstance(content, bytes) authorization_string = authorization_header( access_key=endpoint.access_key, secret_key=endpoint.secret_key, method=str(endpoint.prepared_request.method), content=content, content_type=endpoint.auth_header_content_type, date=date_incorrect_format, request_path=endpoint.prepared_request.path_url, ) headers = { **endpoint_headers, 'Authorization': authorization_string, 'Date': date_incorrect_format, } endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) session = requests.Session() response = session.send(request=endpoint.prepared_request) url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc if netloc == 'cloudreco.vuforia.com': assert response.text == 'Malformed date header.' assert_vwq_failure( response=response, status_code=HTTPStatus.UNAUTHORIZED, content_type='text/plain;charset=iso-8859-1', cache_control=None, www_authenticate='VWS', connection='keep-alive', ) return assert_vws_failure( response=response, status_code=HTTPStatus.BAD_REQUEST, result_code=ResultCodes.FAIL, )
def test_date_out_of_range( self, time_multiplier: int, endpoint: Endpoint, ) -> None: """ If the date header is more than five minutes (target API) or 65 minutes (query API) before or after the request is sent, a `FORBIDDEN` response is returned. Because there is a small delay in sending requests and Vuforia isn't consistent, some leeway is given. """ url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc skew = { 'vws.vuforia.com': _VWS_MAX_TIME_SKEW, 'cloudreco.vuforia.com': _VWQ_MAX_TIME_SKEW, }[netloc] time_difference_from_now = skew + _LEEWAY time_difference_from_now *= time_multiplier gmt = ZoneInfo('GMT') with freeze_time(datetime.now(tz=gmt) + time_difference_from_now): date = rfc_1123_date() endpoint_headers = dict(endpoint.prepared_request.headers) content = endpoint.prepared_request.body or b'' assert isinstance(content, bytes) authorization_string = authorization_header( access_key=endpoint.access_key, secret_key=endpoint.secret_key, method=str(endpoint.prepared_request.method), content=content, content_type=endpoint.auth_header_content_type, date=date, request_path=endpoint.prepared_request.path_url, ) headers = { **endpoint_headers, 'Authorization': authorization_string, 'Date': date, } endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) session = requests.Session() response = session.send( # type: ignore request=endpoint.prepared_request, ) # Even with the query endpoint, we get a JSON response. assert_vws_failure( response=response, status_code=HTTPStatus.FORBIDDEN, result_code=ResultCodes.REQUEST_TIME_TOO_SKEWED, )
def _add_target( vuforia_database: VuforiaDatabase, image_file_failed_state: io.BytesIO, ) -> Endpoint: """ Return details of the endpoint for adding a target. """ image_data = image_file_failed_state.read() image_data_encoded = base64.b64encode(image_data).decode('ascii') date = rfc_1123_date() data: Dict[str, Any] = { 'name': 'example_name', 'width': 1, 'image': image_data_encoded, } request_path = '/targets' content_type = 'application/json' method = POST content = bytes(json.dumps(data), encoding='utf-8') access_key = vuforia_database.server_access_key secret_key = vuforia_database.server_secret_key authorization_string = authorization_header( access_key=access_key, secret_key=secret_key, method=method, content=content, content_type=content_type, date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, 'Content-Type': content_type, } request = requests.Request( method=method, url=urljoin(base=VWS_HOST, url=request_path), headers=headers, data=content, ) prepared_request = request.prepare() return Endpoint( successful_headers_status_code=HTTPStatus.CREATED, successful_headers_result_code=ResultCodes.TARGET_CREATED, prepared_request=prepared_request, access_key=access_key, secret_key=secret_key, )
def _target_api_request( server_access_key: str, server_secret_key: str, method: str, content: bytes, request_path: str, base_vws_url: str, ) -> Response: """ Make a request to the Vuforia Target API. This uses `requests` to make a request against https://vws.vuforia.com. The content type of the request will be `application/json`. Args: server_access_key: A VWS server access key. server_secret_key: A VWS server secret key. method: The HTTP method which will be used in the request. content: The request body which will be used in the request. request_path: The path to the endpoint which will be used in the request. base_vws_url: The base URL for the VWS API. Returns: The response to the request made by `requests`. """ date_string = rfc_1123_date() content_type = 'application/json' signature_string = authorization_header( access_key=server_access_key, secret_key=server_secret_key, method=method, content=content, content_type=content_type, date=date_string, request_path=request_path, ) headers = { 'Authorization': signature_string, 'Date': date_string, 'Content-Type': content_type, } url = urljoin(base=base_vws_url, url=request_path) response = requests.request( method=method, url=url, headers=headers, data=content, ) return response
def _query( vuforia_database: VuforiaDatabase, high_quality_image: io.BytesIO, ) -> Endpoint: """ Return details of the endpoint for making an image recognition query. """ image_content = high_quality_image.read() date = rfc_1123_date() request_path = '/v1/query' files = {'image': ('image.jpeg', image_content, 'image/jpeg')} method = POST content, content_type_header = encode_multipart_formdata( fields=files, ) # type: ignore access_key = vuforia_database.client_access_key secret_key = vuforia_database.client_secret_key authorization_string = authorization_header( access_key=access_key, secret_key=secret_key, method=method, content=content, # Note that this is not the actual Content-Type header value sent. content_type='multipart/form-data', date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, 'Content-Type': content_type_header, } request = requests.Request( method=method, url=urljoin(base=VWQ_HOST, url=request_path), headers=headers, data=content, ) prepared_request = request.prepare() return Endpoint( successful_headers_status_code=HTTPStatus.OK, successful_headers_result_code=ResultCodes.SUCCESS, prepared_request=prepared_request, access_key=access_key, secret_key=secret_key, )
def _update_target( vuforia_database: VuforiaDatabase, target_id: str, vws_client: VWS, ) -> Endpoint: """ Return details of the endpoint for updating a target. """ vws_client.wait_for_target_processed(target_id=target_id) data: Dict[str, Any] = {} request_path = f'/targets/{target_id}' content = bytes(json.dumps(data), encoding='utf-8') content_type = 'application/json' date = rfc_1123_date() method = PUT access_key = vuforia_database.server_access_key secret_key = vuforia_database.server_secret_key authorization_string = authorization_header( access_key=access_key, secret_key=secret_key, method=method, content=content, content_type=content_type, date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, 'Content-Type': content_type, } request = requests.Request( method=method, url=urljoin(base=VWS_HOST, url=request_path), headers=headers, data=content, ) prepared_request = request.prepare() return Endpoint( successful_headers_status_code=HTTPStatus.OK, successful_headers_result_code=ResultCodes.SUCCESS, prepared_request=prepared_request, access_key=access_key, secret_key=secret_key, )
def test_no_date_header( self, endpoint: Endpoint, ) -> None: """ A `BAD_REQUEST` response is returned when no `Date` header is given. """ endpoint_headers = dict(endpoint.prepared_request.headers) content = endpoint.prepared_request.body or b'' assert isinstance(content, bytes) authorization_string = authorization_header( access_key=endpoint.access_key, secret_key=endpoint.secret_key, method=str(endpoint.prepared_request.method), content=content, content_type=endpoint.auth_header_content_type, date='', request_path=endpoint.prepared_request.path_url, ) headers: Dict[str, str] = { **endpoint_headers, 'Authorization': authorization_string, } headers.pop('Date', None) endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) session = requests.Session() response = session.send(request=endpoint.prepared_request) url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc if netloc == 'cloudreco.vuforia.com': expected_content_type = 'text/plain;charset=iso-8859-1' assert response.text == 'Date header required.' assert_vwq_failure( response=response, status_code=HTTPStatus.BAD_REQUEST, content_type=expected_content_type, cache_control=None, www_authenticate=None, connection='keep-alive', ) return assert_vws_failure( response=response, status_code=HTTPStatus.BAD_REQUEST, result_code=ResultCodes.FAIL, )
def _get_duplicates( vuforia_database: VuforiaDatabase, target_id: str, vws_client: VWS, ) -> Endpoint: """ Return details of the endpoint for getting potential duplicates of a target. """ vws_client.wait_for_target_processed(target_id=target_id) date = rfc_1123_date() request_path = f'/duplicates/{target_id}' method = GET content = b'' access_key = vuforia_database.server_access_key secret_key = vuforia_database.server_secret_key authorization_string = authorization_header( access_key=access_key, secret_key=secret_key, method=method, content=content, content_type='', date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, } request = requests.Request( method=method, url=urljoin(base=VWS_HOST, url=request_path), headers=headers, data=content, ) prepared_request = request.prepare() return Endpoint( successful_headers_status_code=HTTPStatus.OK, successful_headers_result_code=ResultCodes.SUCCESS, prepared_request=prepared_request, access_key=access_key, secret_key=secret_key, )
def update_target( vuforia_database: VuforiaDatabase, data: Dict[str, Any], target_id: str, content_type: str = 'application/json', ) -> Response: """ Make a request to the endpoint to update a target. Args: vuforia_database: The credentials to use to connect to Vuforia. data: The data to send, in JSON format, to the endpoint. target_id: The ID of the target to update. content_type: The `Content-Type` header to use. Returns: The response returned by the API. """ date = rfc_1123_date() request_path = '/targets/' + target_id content = bytes(json.dumps(data), encoding='utf-8') authorization_string = authorization_header( access_key=vuforia_database.server_access_key, secret_key=vuforia_database.server_secret_key, method=PUT, content=content, content_type=content_type, date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, 'Content-Type': content_type, } response = requests.request( method=PUT, url=urljoin('https://vws.vuforia.com/', request_path), headers=headers, data=content, ) return response
def _target_list(vuforia_database: VuforiaDatabase) -> Endpoint: """ Return details of the endpoint for getting a list of targets. """ date = rfc_1123_date() request_path = '/targets' method = GET content = b'' access_key = vuforia_database.server_access_key secret_key = vuforia_database.server_secret_key authorization_string = authorization_header( access_key=access_key, secret_key=secret_key, method=method, content=content, content_type='', date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, } request = requests.Request( method=method, url=urljoin(base=VWS_HOST, url=request_path), headers=headers, data=content, ) prepared_request = request.prepare() return Endpoint( successful_headers_status_code=HTTPStatus.OK, successful_headers_result_code=ResultCodes.SUCCESS, prepared_request=prepared_request, access_key=access_key, secret_key=secret_key, )
def get_database_matching_server_keys( request_headers: Dict[str, str], request_body: bytes | None, request_method: str, request_path: str, databases: Iterable[VuforiaDatabase], ) -> VuforiaDatabase | None: """ Return which, if any, of the given databases is being accessed by the given server request. Args: request_headers: The headers sent with the request. request_body: The request body. request_method: The HTTP method of the request. request_path: The path of the request. databases: The databases to check for matches. Returns: The database being accessed by the given server request. """ content_type = request_headers.get('Content-Type', '').split(';')[0] auth_header = request_headers.get('Authorization') content = request_body or b'' date = request_headers.get('Date', '') for database in databases: expected_authorization_header = authorization_header( access_key=database.server_access_key, secret_key=database.server_secret_key, method=request_method, content=content, content_type=content_type, date=date, request_path=request_path, ) if auth_header == expected_authorization_header: return database return None
def query( self, image: io.BytesIO, max_num_results: int = 1, include_target_data: CloudRecoIncludeTargetData = ( CloudRecoIncludeTargetData.TOP ), ) -> List[QueryResult]: """ Use the Vuforia Web Query API to make an Image Recognition Query. See https://library.vuforia.com/articles/Solution/How-To-Perform-an-Image-Recognition-Query for parameter details. Args: image: The image to make a query against. max_num_results: The maximum number of matching targets to be returned. include_target_data: Indicates if target_data records shall be returned for the matched targets. Accepted values are top (default value, only return target_data for top ranked match), none (return no target_data), all (for all matched targets). Raises: ~vws.exceptions.cloud_reco_exceptions.AuthenticationFailure: The client access key pair is not correct. ~vws.exceptions.cloud_reco_exceptions.MaxNumResultsOutOfRange: ``max_num_results`` is not within the range (1, 50). ~vws.exceptions.cloud_reco_exceptions.MatchProcessing: The given image matches a target which was recently added, updated or deleted and Vuforia returns an error in this case. ~vws.exceptions.cloud_reco_exceptions.InactiveProject: The project is inactive. ~vws.exceptions.custom_exceptions.ConnectionErrorPossiblyImageTooLarge: The given image is too large. ~vws.exceptions.cloud_reco_exceptions.RequestTimeTooSkewed: There is an error with the time sent to Vuforia. ~vws.exceptions.cloud_reco_exceptions.BadImage: There is a problem with the given image. For example, it must be a JPEG or PNG file in the grayscale or RGB color space. Returns: An ordered list of target details of matching targets. """ image_content = image.getvalue() body = { 'image': ('image.jpeg', image_content, 'image/jpeg'), 'max_num_results': (None, int(max_num_results), 'text/plain'), 'include_target_data': ( None, include_target_data.value, 'text/plain', ), } date = rfc_1123_date() request_path = '/v1/query' content, content_type_header = encode_multipart_formdata(body) method = 'POST' authorization_string = authorization_header( access_key=self._client_access_key, secret_key=self._client_secret_key, method=method, content=content, # Note that this is not the actual Content-Type header value sent. content_type='multipart/form-data', date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, 'Content-Type': content_type_header, } try: response = requests.request( method=method, url=urljoin(base=self._base_vwq_url, url=request_path), headers=headers, data=content, ) except requests.exceptions.ConnectionError as exc: raise ConnectionErrorPossiblyImageTooLarge( request=exc.request, response=exc.response, ) from exc if 'Integer out of range' in response.text: raise MaxNumResultsOutOfRange(response=response) if 'No content to map due to end-of-input' in response.text: raise MatchProcessing(response=response) result_code = response.json()['result_code'] if result_code != 'Success': exception = { 'AuthenticationFailure': AuthenticationFailure, 'BadImage': BadImage, 'InactiveProject': InactiveProject, 'RequestTimeTooSkewed': RequestTimeTooSkewed, }[result_code] raise exception(response=response) result = [] result_list = list(response.json()['results']) for item in result_list: target_data: Optional[TargetData] = None if 'target_data' in item: target_data_dict = item['target_data'] metadata = target_data_dict['application_metadata'] timestamp_string = target_data_dict['target_timestamp'] target_timestamp = datetime.datetime.utcfromtimestamp( timestamp_string, ) target_data = TargetData( name=target_data_dict['name'], application_metadata=metadata, target_timestamp=target_timestamp, ) query_result = QueryResult( target_id=item['target_id'], target_data=target_data, ) result.append(query_result) return result
def test_invalid_json( self, endpoint: Endpoint, date_skew_minutes: int, ) -> None: """ Giving invalid JSON to endpoints returns error responses. """ date_is_skewed = not date_skew_minutes == 0 content = b'a' gmt = ZoneInfo('GMT') now = datetime.now(tz=gmt) time_to_freeze = now + timedelta(minutes=date_skew_minutes) with freeze_time(time_to_freeze): date = rfc_1123_date() endpoint_headers = dict(endpoint.prepared_request.headers) authorization_string = authorization_header( access_key=endpoint.access_key, secret_key=endpoint.secret_key, method=str(endpoint.prepared_request.method), content=content, content_type=endpoint.auth_header_content_type, date=date, request_path=endpoint.prepared_request.path_url, ) headers = { **endpoint_headers, 'Authorization': authorization_string, 'Date': date, } endpoint.prepared_request.body = content endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) endpoint.prepared_request.prepare_content_length(body=content) session = requests.Session() response = session.send( # type: ignore request=endpoint.prepared_request, ) takes_json_data = ( endpoint.auth_header_content_type == 'application/json' ) assert_valid_date_header(response=response) if date_is_skewed and takes_json_data: # On the real implementation, we get `HTTPStatus.FORBIDDEN` and # `REQUEST_TIME_TOO_SKEWED`. # See https://github.com/VWS-Python/vws-python-mock/issues/4 for # implementing this on them mock. return if not date_is_skewed and takes_json_data: assert_vws_failure( response=response, status_code=HTTPStatus.BAD_REQUEST, result_code=ResultCodes.FAIL, ) return assert response.status_code == HTTPStatus.BAD_REQUEST url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc if netloc == 'cloudreco.vuforia.com': assert_vwq_failure( response=response, status_code=HTTPStatus.BAD_REQUEST, content_type='text/html;charset=UTF-8', ) expected_text = ( 'java.lang.RuntimeException: RESTEASY007500: ' 'Could find no Content-Disposition header within part' ) assert response.text == expected_text return assert response.text == '' assert 'Content-Type' not in response.headers
def test_does_not_take_data( self, endpoint: Endpoint, ) -> None: """ Giving JSON to endpoints which do not take any JSON data returns error responses. """ if ( endpoint.prepared_request.headers.get( 'Content-Type', ) == 'application/json' ): return content = bytes(json.dumps({'key': 'value'}), encoding='utf-8') content_type = 'application/json' date = rfc_1123_date() endpoint_headers = dict(endpoint.prepared_request.headers) authorization_string = authorization_header( access_key=endpoint.access_key, secret_key=endpoint.secret_key, method=str(endpoint.prepared_request.method), content=content, content_type=content_type, date=date, request_path=endpoint.prepared_request.path_url, ) headers = { **endpoint_headers, 'Authorization': authorization_string, 'Date': date, 'Content-Type': content_type, } endpoint.prepared_request.body = content endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) endpoint.prepared_request.prepare_content_length(body=content) session = requests.Session() response = session.send( # type: ignore request=endpoint.prepared_request, ) url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc if netloc == 'cloudreco.vuforia.com': # The multipart/formdata boundary is no longer in the given # content. assert response.text == '' assert_vwq_failure( response=response, status_code=HTTPStatus.UNSUPPORTED_MEDIA_TYPE, content_type=None, ) return assert response.status_code == HTTPStatus.BAD_REQUEST assert response.text == '' assert 'Content-Type' not in response.headers