Commit 59a40a2a authored by Luc Yriarte's avatar Luc Yriarte
Browse files

Merge branch 'storage-update-recordIdVersions' into 'master'

Storage client update with record id versions

See merge request !7
parents 33480672 cbc9c251
Pipeline #73466 passed with stage
in 33 seconds
......@@ -835,6 +835,13 @@
"format" : "int32",
"description" : "Number of records ingested successfully."
},
"recordIdVersions" : {
"type" : "array",
"description" : "List of ingested record id versions.",
"items" : {
"type" : "string"
}
},
"recordIds" : {
"type" : "array",
"description" : "List of ingested record id.",
......
......@@ -25,11 +25,7 @@ class _QueryApi:
body = jsonable_encoder(multi_record_ids)
return self.api_client.request(
type_=m.MultiRecordInfo,
method="POST",
url="/v2/query/records",
headers=headers,
json=body,
type_=m.MultiRecordInfo, method="POST", url="/v2/query/records", headers=headers, json=body
)
def _build_for_fetch_records_with_optional_conversion(
......@@ -43,11 +39,7 @@ class _QueryApi:
body = jsonable_encoder(multi_record_request)
return self.api_client.request(
type_=m.MultiRecordResponse,
method="POST",
url="/v2/query/records:batch",
headers=headers,
json=body,
type_=m.MultiRecordResponse, method="POST", url="/v2/query/records:batch", headers=headers, json=body
)
def _build_for_get_all_kinds(
......@@ -73,11 +65,7 @@ class _QueryApi:
)
def _build_for_get_all_record_from_kind(
self,
data_partition_id: str,
cursor: str = None,
limit: int = None,
kind: str = None,
self, data_partition_id: str, cursor: str = None, limit: int = None, kind: str = None
) -> Awaitable[m.DatastoreQueryResult]:
"""
The API returns a list of all record ids which belong to the specified kind. Required roles: 'users.datalake.ops'.
......@@ -125,8 +113,7 @@ class AsyncQueryApi(_QueryApi):
Fetch records and do corresponding conversion as user requested, no more than 20 records per request.
"""
return await self._build_for_fetch_records_with_optional_conversion(
data_partition_id=data_partition_id,
multi_record_request=multi_record_request,
data_partition_id=data_partition_id, multi_record_request=multi_record_request
)
async def get_all_kinds(
......@@ -135,16 +122,10 @@ class AsyncQueryApi(_QueryApi):
"""
The API returns a list of all kinds in the specific {Data-Partition-Id}. Required roles: 'users.datalake.editors' or 'users.datalake.admins'.
"""
return await self._build_for_get_all_kinds(
data_partition_id=data_partition_id, cursor=cursor, limit=limit
)
return await self._build_for_get_all_kinds(data_partition_id=data_partition_id, cursor=cursor, limit=limit)
async def get_all_record_from_kind(
self,
data_partition_id: str,
cursor: str = None,
limit: int = None,
kind: str = None,
self, data_partition_id: str, cursor: str = None, limit: int = None, kind: str = None
) -> m.DatastoreQueryResult:
"""
The API returns a list of all record ids which belong to the specified kind. Required roles: 'users.datalake.ops'.
......@@ -155,9 +136,7 @@ class AsyncQueryApi(_QueryApi):
class SyncQueryApi(_QueryApi):
def fetch_records(
self, data_partition_id: str, multi_record_ids: m.MultiRecordIds = None
) -> m.MultiRecordInfo:
def fetch_records(self, data_partition_id: str, multi_record_ids: m.MultiRecordIds = None) -> m.MultiRecordInfo:
"""
The API fetches multiple records at once. Required roles: 'users.datalake.viewers' or 'users.datalake.editors' or 'users.datalake.admins'.
"""
......@@ -173,28 +152,19 @@ class SyncQueryApi(_QueryApi):
Fetch records and do corresponding conversion as user requested, no more than 20 records per request.
"""
coroutine = self._build_for_fetch_records_with_optional_conversion(
data_partition_id=data_partition_id,
multi_record_request=multi_record_request,
data_partition_id=data_partition_id, multi_record_request=multi_record_request
)
return get_event_loop().run_until_complete(coroutine)
def get_all_kinds(
self, data_partition_id: str, cursor: str = None, limit: int = None
) -> m.DatastoreQueryResult:
def get_all_kinds(self, data_partition_id: str, cursor: str = None, limit: int = None) -> m.DatastoreQueryResult:
"""
The API returns a list of all kinds in the specific {Data-Partition-Id}. Required roles: 'users.datalake.editors' or 'users.datalake.admins'.
"""
coroutine = self._build_for_get_all_kinds(
data_partition_id=data_partition_id, cursor=cursor, limit=limit
)
coroutine = self._build_for_get_all_kinds(data_partition_id=data_partition_id, cursor=cursor, limit=limit)
return get_event_loop().run_until_complete(coroutine)
def get_all_record_from_kind(
self,
data_partition_id: str,
cursor: str = None,
limit: int = None,
kind: str = None,
self, data_partition_id: str, cursor: str = None, limit: int = None, kind: str = None
) -> m.DatastoreQueryResult:
"""
The API returns a list of all record ids which belong to the specified kind. Required roles: 'users.datalake.ops'.
......
......@@ -15,10 +15,7 @@ class _RecordsApi:
self.api_client = api_client
def _build_for_create_or_update_records(
self,
data_partition_id: str,
skipdupes: bool = None,
record: List[m.Record] = None,
self, data_partition_id: str, skipdupes: bool = None, record: List[m.Record] = None
) -> Awaitable[m.CreateUpdateRecordsResponse]:
"""
The API represents the main injection mechanism into the Data Ecosystem. It allows records creation and/or update. When no record id is provided or when the provided id is not already present in the Data Ecosystem then a new record is created. If the id is related to an existing record in the Data Ecosystem then an update operation takes place and a new version of the record is created. Required roles: 'users.datalake.editors' or 'users.datalake.admins'.
......@@ -29,9 +26,7 @@ class _RecordsApi:
headers = {"data-partition-id": str(data_partition_id)}
#Warning this part is edited manually, keeping unset values breaks the storage service
#POST new data seems ok but when getting them storage 500 if for instance we push createTime with a null value
body = jsonable_encoder(record, exclude_unset=True)
body = jsonable_encoder(record)
return self.api_client.request(
type_=m.CreateUpdateRecordsResponse,
......@@ -42,9 +37,7 @@ class _RecordsApi:
json=body,
)
def _build_for_delete_record(
self, id: str, data_partition_id: str, body: Any = None
) -> Awaitable[None]:
def _build_for_delete_record(self, id: str, data_partition_id: str, body: Any = None) -> Awaitable[None]:
"""
The API performs a logical deletion of the given record. This operation can be reverted later. Required roles: 'users.datalake.editors' or 'users.datalake.admins'.
"""
......@@ -63,9 +56,7 @@ class _RecordsApi:
json=body,
)
def _build_for_get_all_record_versions(
self, id: str, data_partition_id: str
) -> Awaitable[m.RecordVersions]:
def _build_for_get_all_record_versions(self, id: str, data_partition_id: str) -> Awaitable[m.RecordVersions]:
"""
The API returns a list containing all versions for the given record id. Required roles: 'users.datalake.viewers' or 'users.datalake.editors' or 'users.datalake.admins'.
"""
......@@ -91,9 +82,7 @@ class _RecordsApi:
query_params = {}
if attribute is not None:
query_params["attribute"] = [
str(attribute_item) for attribute_item in attribute
]
query_params["attribute"] = [str(attribute_item) for attribute_item in attribute]
headers = {"data-partition-id": str(data_partition_id)}
......@@ -116,9 +105,7 @@ class _RecordsApi:
query_params = {}
if attribute is not None:
query_params["attribute"] = [
str(attribute_item) for attribute_item in attribute
]
query_params["attribute"] = [str(attribute_item) for attribute_item in attribute]
headers = {"data-partition-id": str(data_partition_id)}
......@@ -131,9 +118,7 @@ class _RecordsApi:
headers=headers,
)
def _build_for_purge_record(
self, id: str, data_partition_id: str
) -> Awaitable[None]:
def _build_for_purge_record(self, id: str, data_partition_id: str) -> Awaitable[None]:
"""
The API performs the physical deletion of the given record and all of its versions. This operation cannot be undone. Required roles: 'users.datalake.ops'.
"""
......@@ -158,10 +143,7 @@ class AsyncRecordsApi(_RecordsApi):
await self.api_client.close()
async def create_or_update_records(
self,
data_partition_id: str,
skipdupes: bool = None,
record: List[m.Record] = None,
self, data_partition_id: str, skipdupes: bool = None, record: List[m.Record] = None
) -> m.CreateUpdateRecordsResponse:
"""
The API represents the main injection mechanism into the Data Ecosystem. It allows records creation and/or update. When no record id is provided or when the provided id is not already present in the Data Ecosystem then a new record is created. If the id is related to an existing record in the Data Ecosystem then an update operation takes place and a new version of the record is created. Required roles: 'users.datalake.editors' or 'users.datalake.admins'.
......@@ -170,35 +152,23 @@ class AsyncRecordsApi(_RecordsApi):
data_partition_id=data_partition_id, skipdupes=skipdupes, record=record
)
async def delete_record(
self, id: str, data_partition_id: str, body: Any = None
) -> None:
async def delete_record(self, id: str, data_partition_id: str, body: Any = None) -> None:
"""
The API performs a logical deletion of the given record. This operation can be reverted later. Required roles: 'users.datalake.editors' or 'users.datalake.admins'.
"""
return await self._build_for_delete_record(
id=id, data_partition_id=data_partition_id, body=body
)
return await self._build_for_delete_record(id=id, data_partition_id=data_partition_id, body=body)
async def get_all_record_versions(
self, id: str, data_partition_id: str
) -> m.RecordVersions:
async def get_all_record_versions(self, id: str, data_partition_id: str) -> m.RecordVersions:
"""
The API returns a list containing all versions for the given record id. Required roles: 'users.datalake.viewers' or 'users.datalake.editors' or 'users.datalake.admins'.
"""
return await self._build_for_get_all_record_versions(
id=id, data_partition_id=data_partition_id
)
return await self._build_for_get_all_record_versions(id=id, data_partition_id=data_partition_id)
async def get_record(
self, id: str, data_partition_id: str, attribute: List[str] = None
) -> m.Record:
async def get_record(self, id: str, data_partition_id: str, attribute: List[str] = None) -> m.Record:
"""
This API returns the latest version of the given record. Required roles: 'users.datalake.viewers' or 'users.datalake.editors' or 'users.datalake.admins'.
"""
return await self._build_for_get_record(
id=id, data_partition_id=data_partition_id, attribute=attribute
)
return await self._build_for_get_record(id=id, data_partition_id=data_partition_id, attribute=attribute)
async def get_record_version(
self, id: str, version: int, data_partition_id: str, attribute: List[str] = None
......@@ -207,27 +177,19 @@ class AsyncRecordsApi(_RecordsApi):
The API retrieves the specific version of the given record. Required roles: 'users.datalake.viewers' or 'users.datalake.editors' or 'users.datalake.admins'.
"""
return await self._build_for_get_record_version(
id=id,
version=version,
data_partition_id=data_partition_id,
attribute=attribute,
id=id, version=version, data_partition_id=data_partition_id, attribute=attribute
)
async def purge_record(self, id: str, data_partition_id: str) -> None:
"""
The API performs the physical deletion of the given record and all of its versions. This operation cannot be undone. Required roles: 'users.datalake.ops'.
"""
return await self._build_for_purge_record(
id=id, data_partition_id=data_partition_id
)
return await self._build_for_purge_record(id=id, data_partition_id=data_partition_id)
class SyncRecordsApi(_RecordsApi):
def create_or_update_records(
self,
data_partition_id: str,
skipdupes: bool = None,
record: List[m.Record] = None,
self, data_partition_id: str, skipdupes: bool = None, record: List[m.Record] = None
) -> m.CreateUpdateRecordsResponse:
"""
The API represents the main injection mechanism into the Data Ecosystem. It allows records creation and/or update. When no record id is provided or when the provided id is not already present in the Data Ecosystem then a new record is created. If the id is related to an existing record in the Data Ecosystem then an update operation takes place and a new version of the record is created. Required roles: 'users.datalake.editors' or 'users.datalake.admins'.
......@@ -241,31 +203,21 @@ class SyncRecordsApi(_RecordsApi):
"""
The API performs a logical deletion of the given record. This operation can be reverted later. Required roles: 'users.datalake.editors' or 'users.datalake.admins'.
"""
coroutine = self._build_for_delete_record(
id=id, data_partition_id=data_partition_id, body=body
)
coroutine = self._build_for_delete_record(id=id, data_partition_id=data_partition_id, body=body)
return get_event_loop().run_until_complete(coroutine)
def get_all_record_versions(
self, id: str, data_partition_id: str
) -> m.RecordVersions:
def get_all_record_versions(self, id: str, data_partition_id: str) -> m.RecordVersions:
"""
The API returns a list containing all versions for the given record id. Required roles: 'users.datalake.viewers' or 'users.datalake.editors' or 'users.datalake.admins'.
"""
coroutine = self._build_for_get_all_record_versions(
id=id, data_partition_id=data_partition_id
)
coroutine = self._build_for_get_all_record_versions(id=id, data_partition_id=data_partition_id)
return get_event_loop().run_until_complete(coroutine)
def get_record(
self, id: str, data_partition_id: str, attribute: List[str] = None
) -> m.Record:
def get_record(self, id: str, data_partition_id: str, attribute: List[str] = None) -> m.Record:
"""
This API returns the latest version of the given record. Required roles: 'users.datalake.viewers' or 'users.datalake.editors' or 'users.datalake.admins'.
"""
coroutine = self._build_for_get_record(
id=id, data_partition_id=data_partition_id, attribute=attribute
)
coroutine = self._build_for_get_record(id=id, data_partition_id=data_partition_id, attribute=attribute)
return get_event_loop().run_until_complete(coroutine)
def get_record_version(
......@@ -275,10 +227,7 @@ class SyncRecordsApi(_RecordsApi):
The API retrieves the specific version of the given record. Required roles: 'users.datalake.viewers' or 'users.datalake.editors' or 'users.datalake.admins'.
"""
coroutine = self._build_for_get_record_version(
id=id,
version=version,
data_partition_id=data_partition_id,
attribute=attribute,
id=id, version=version, data_partition_id=data_partition_id, attribute=attribute
)
return get_event_loop().run_until_complete(coroutine)
......@@ -286,7 +235,5 @@ class SyncRecordsApi(_RecordsApi):
"""
The API performs the physical deletion of the given record and all of its versions. This operation cannot be undone. Required roles: 'users.datalake.ops'.
"""
coroutine = self._build_for_purge_record(
id=id, data_partition_id=data_partition_id
)
coroutine = self._build_for_purge_record(id=id, data_partition_id=data_partition_id)
return get_event_loop().run_until_complete(coroutine)
......@@ -14,9 +14,7 @@ class _SchemasApi:
def __init__(self, api_client: "ApiClient"):
self.api_client = api_client
def _build_for_create_schema(
self, data_partition_id: str, schema: m.Schema = None
) -> Awaitable[None]:
def _build_for_create_schema(self, data_partition_id: str, schema: m.Schema = None) -> Awaitable[None]:
"""
The API allows the creation of a new schema for the given kind. Required roles: 'users.datalake.editors' or 'users.datalake.admins'.
"""
......@@ -24,13 +22,9 @@ class _SchemasApi:
body = jsonable_encoder(schema)
return self.api_client.request(
type_=None, method="POST", url="/v2/schemas", headers=headers, json=body
)
return self.api_client.request(type_=None, method="POST", url="/v2/schemas", headers=headers, json=body)
def _build_for_delete_a_schema(
self, kind: str, data_partition_id: str
) -> Awaitable[None]:
def _build_for_delete_a_schema(self, kind: str, data_partition_id: str) -> Awaitable[None]:
"""
The API deletes the schema of the given kind, which must follow the naming convetion {Data-Partition-Id}:{dataset}:{type}:{version} format. This operation cannot be undone. Required roles: 'users.datalake.ops'.
"""
......@@ -46,9 +40,7 @@ class _SchemasApi:
headers=headers,
)
def _build_for_get_schema(
self, kind: str, data_partition_id: str
) -> Awaitable[m.Schema]:
def _build_for_get_schema(self, kind: str, data_partition_id: str) -> Awaitable[m.Schema]:
"""
The API returns the schema specified byt the given kind, which must follow the naming convention {Data-Partition-Id}:{dataset}:{type}:{version}. Required roles: 'users.datalake.viewers' or 'users.datalake.editors' or 'users.datalake.admins'.
"""
......@@ -72,31 +64,23 @@ class AsyncSchemasApi(_SchemasApi):
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.api_client.close()
async def create_schema(
self, data_partition_id: str, schema: m.Schema = None
) -> None:
async def create_schema(self, data_partition_id: str, schema: m.Schema = None) -> None:
"""
The API allows the creation of a new schema for the given kind. Required roles: 'users.datalake.editors' or 'users.datalake.admins'.
"""
return await self._build_for_create_schema(
data_partition_id=data_partition_id, schema=schema
)
return await self._build_for_create_schema(data_partition_id=data_partition_id, schema=schema)
async def delete_a_schema(self, kind: str, data_partition_id: str) -> None:
"""
The API deletes the schema of the given kind, which must follow the naming convetion {Data-Partition-Id}:{dataset}:{type}:{version} format. This operation cannot be undone. Required roles: 'users.datalake.ops'.
"""
return await self._build_for_delete_a_schema(
kind=kind, data_partition_id=data_partition_id
)
return await self._build_for_delete_a_schema(kind=kind, data_partition_id=data_partition_id)
async def get_schema(self, kind: str, data_partition_id: str) -> m.Schema:
"""
The API returns the schema specified byt the given kind, which must follow the naming convention {Data-Partition-Id}:{dataset}:{type}:{version}. Required roles: 'users.datalake.viewers' or 'users.datalake.editors' or 'users.datalake.admins'.
"""
return await self._build_for_get_schema(
kind=kind, data_partition_id=data_partition_id
)
return await self._build_for_get_schema(kind=kind, data_partition_id=data_partition_id)
class SyncSchemasApi(_SchemasApi):
......@@ -104,25 +88,19 @@ class SyncSchemasApi(_SchemasApi):
"""
The API allows the creation of a new schema for the given kind. Required roles: 'users.datalake.editors' or 'users.datalake.admins'.
"""
coroutine = self._build_for_create_schema(
data_partition_id=data_partition_id, schema=schema
)
coroutine = self._build_for_create_schema(data_partition_id=data_partition_id, schema=schema)
return get_event_loop().run_until_complete(coroutine)
def delete_a_schema(self, kind: str, data_partition_id: str) -> None:
"""
The API deletes the schema of the given kind, which must follow the naming convetion {Data-Partition-Id}:{dataset}:{type}:{version} format. This operation cannot be undone. Required roles: 'users.datalake.ops'.
"""
coroutine = self._build_for_delete_a_schema(
kind=kind, data_partition_id=data_partition_id
)
coroutine = self._build_for_delete_a_schema(kind=kind, data_partition_id=data_partition_id)
return get_event_loop().run_until_complete(coroutine)
def get_schema(self, kind: str, data_partition_id: str) -> m.Schema:
"""
The API returns the schema specified byt the given kind, which must follow the naming convention {Data-Partition-Id}:{dataset}:{type}:{version}. Required roles: 'users.datalake.viewers' or 'users.datalake.editors' or 'users.datalake.admins'.
"""
coroutine = self._build_for_get_schema(
kind=kind, data_partition_id=data_partition_id
)
coroutine = self._build_for_get_schema(kind=kind, data_partition_id=data_partition_id)
return get_event_loop().run_until_complete(coroutine)
......@@ -8,11 +8,7 @@ from pydantic import ValidationError
from odes_storage.api.query_api import AsyncQueryApi, SyncQueryApi
from odes_storage.api.records_api import AsyncRecordsApi, SyncRecordsApi
from odes_storage.api.schemas_api import AsyncSchemasApi, SyncSchemasApi
from odes_storage.exceptions import (
ResponseHandlingException,
ResponseValidationError,
UnexpectedResponse,
)
from odes_storage.exceptions import ResponseHandlingException, ResponseValidationError, UnexpectedResponse
ClientT = TypeVar("ClientT", bound="ApiClient")
......@@ -51,36 +47,18 @@ class ApiClient:
@overload
async def request(
self,
*,
type_: Type[T],
method: str,
url: str,
path_params: Dict[str, Any] = None,
**kwargs: Any,
self, *, type_: Type[T], method: str, url: str, path_params: Dict[str, Any] = None, **kwargs: Any
) -> T:
...
@overload # noqa F811
async def request(
self,
*,
type_: None,
method: str,
url: str,
path_params: Dict[str, Any] = None,
**kwargs: Any,
self, *, type_: None, method: str, url: str, path_params: Dict[str, Any] = None, **kwargs: Any
) -> None:
...
async def request( # noqa F811
self,
*,
type_: Any,
method: str,
url: str,
path_params: Dict[str, Any] = None,
**kwargs: Any,
self, *, type_: Any, method: str, url: str, path_params: Dict[str, Any] = None, **kwargs: Any
) -> Any:
if path_params is None:
path_params = {}
......
......@@ -29,9 +29,7 @@ class AuthState(BaseModel):
def get_login_request(self) -> Optional[AccessTokenRequest]:
if self.username is None or self.password is None:
return None
return AccessTokenRequest(
username=self.username, password=self.password, scope=self.scope
)
return AccessTokenRequest(username=self.username, password=self.password, scope=self.scope)
def get_refresh_request(self) -> Optional[RefreshTokenRequest]:
if self.refresh_token is None:
......@@ -48,9 +46,7 @@ class AuthState(BaseModel):
self.refresh_token = token_success_response.refresh_token
self.scope = token_success_response.scope
if token_success_response.expires_in is not None:
self.expires_at = datetime.utcnow() + timedelta(
seconds=token_success_response.expires_in
)
self.expires_at = datetime.utcnow() + timedelta(seconds=token_success_response.expires_in)
class AuthMiddleware:
......@@ -72,9 +68,7 @@ class AuthMiddleware:
if access_token_request is None:
return None
with suppress(UnexpectedResponse):
token_response = await self.flow_client.request_access_token(
access_token_request
)
token_response = await self.flow_client.request_access_token(access_token_request)
if isinstance(token_response, TokenSuccessResponse):
self.update_auth_state(token_response)
return token_response
......@@ -85,9 +79,7 @@ class AuthMiddleware:
if refresh_token_request is None:
return None
with suppress(UnexpectedResponse):
token_response = await self.flow_client.request_refresh_token(
refresh_token_request
)
token_response = await self.flow_client.request_refresh_token(refresh_token_request)
if isinstance(token_response, TokenSuccessResponse):
self.update_auth_state(token_response)
return token_response
......
......@@ -4,6 +4,7 @@
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**record_count** | **int** | Number of records ingested successfully. | [optional]
**record_id_versions** | **List[str]** | List of ingested record id versions. | [optional]
**record_ids** | **List[str]** | List of ingested record id. | [optional]
**skipped_record_ids** | **List[str]** | List of record id that skipped update because it was a duplicate of the existing record. | [optional]
......
......@@ -17,13 +17,7 @@ class UnexpectedResponse(ApiException):
Encapsulates response status code, content and headers
"""
def __init__(
self,
status_code: Optional[int],
reason_phrase: str,
content: bytes,
headers: Headers,
) -> None:
def __init__(self, status_code: Optional[int], reason_phrase: str, content: bytes, headers: Headers) -> None:
self.status_code = status_code
self.reason_phrase = reason_phrase
self.content = content
......@@ -45,11 +39,7 @@ class UnexpectedResponse(ApiException):
else:
reason_phrase_str = f"({self.reason_phrase})"
status_str = f"{status_code_str} {reason_phrase_str}".strip()
short_content = (
self.content
if len(self.content) <= MAX_CONTENT
else self.content[: MAX_CONTENT - 3] + b" ..."
)
short_content = self.content if len(self.content) <= MAX_CONTENT else self.content[: MAX_CONTENT - 3] + b" ..."
raw_content_str = f"Raw response content:\n{short_content!r}"
return f"Unexpected Response: {status_str}\n{raw_content_str}"
......@@ -64,25 +54,15 @@ class ResponseValidationError(ApiException):
Encapsulates source exception and response status code, content and headers
"""
def __init__(
self,
source: Exception,
status_code: Optional[int],
content: str,
headers: dict = None,
) -> None:
def __init__(self, source: Exception, status_code: Optional[int], content: str, headers: dict = None) -> None:
self.source = source
self.status_code = status_code
self.content = content
self.headers = headers
@staticmethod
def for_exception(
source: Exception, response: Response
) -> "ResponseValidationError":