From 43be338c4f4c14d669b0362f8b6d66f87be24ac9 Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen Date: Thu, 27 Feb 2020 18:11:42 +0100 Subject: [PATCH 1/6] Rename used Error model to OptimadeError --- openapi/index_openapi.json | 248 ++++++++++++------------- openapi/openapi.json | 258 +++++++++++++------------- optimade/models/optimade_json.py | 8 +- optimade/models/toplevel.py | 4 +- optimade/server/exception_handlers.py | 10 +- 5 files changed, 265 insertions(+), 263 deletions(-) diff --git a/openapi/index_openapi.json b/openapi/index_openapi.json index 612037e06..6dfa66746 100644 --- a/openapi/index_openapi.json +++ b/openapi/index_openapi.json @@ -411,6 +411,65 @@ }, "description": "Contains key-value pairs representing the entry's properties." }, + "Error": { + "title": "Error", + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string", + "description": "A unique identifier for this particular occurrence of the problem." + }, + "links": { + "title": "Links", + "allOf": [ + { + "$ref": "#/components/schemas/ErrorLinks" + } + ], + "description": "A links object storing about" + }, + "status": { + "title": "Status", + "type": "string", + "description": "the HTTP status code applicable to this problem, expressed as a string value." + }, + "code": { + "title": "Code", + "type": "string", + "description": "an application-specific error code, expressed as a string value." + }, + "title": { + "title": "Title", + "type": "string", + "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization." + }, + "detail": { + "title": "Detail", + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "source": { + "title": "Source", + "allOf": [ + { + "$ref": "#/components/schemas/ErrorSource" + } + ], + "description": "An object containing references to the source of the error" + }, + "meta": { + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/Meta" + } + ], + "description": "a meta object containing non-standard meta-information about the error." + } + }, + "description": "An error response" + }, "ErrorLinks": { "title": "ErrorLinks", "type": "object", @@ -471,7 +530,7 @@ "title": "Errors", "type": "array", "items": { - "$ref": "#/components/schemas/optimade__models__optimade_json__Error" + "$ref": "#/components/schemas/OptimadeError" } }, "included": { @@ -711,7 +770,7 @@ "uniqueItems": true, "type": "array", "items": { - "$ref": "#/components/schemas/optimade__models__jsonapi__Error" + "$ref": "#/components/schemas/Error" }, "description": "A list of errors" }, @@ -964,7 +1023,7 @@ "uniqueItems": true, "type": "array", "items": { - "$ref": "#/components/schemas/optimade__models__jsonapi__Error" + "$ref": "#/components/schemas/Error" }, "description": "A list of errors" }, @@ -1012,6 +1071,68 @@ "properties": {}, "description": "Non-standard meta-information that can not be represented as an attribute or relationship." }, + "OptimadeError": { + "title": "OptimadeError", + "required": [ + "detail" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string", + "description": "A unique identifier for this particular occurrence of the problem." + }, + "links": { + "title": "Links", + "allOf": [ + { + "$ref": "#/components/schemas/ErrorLinks" + } + ], + "description": "A links object storing about" + }, + "status": { + "title": "Status", + "type": "string", + "description": "the HTTP status code applicable to this problem, expressed as a string value." + }, + "code": { + "title": "Code", + "type": "string", + "description": "an application-specific error code, expressed as a string value." + }, + "title": { + "title": "Title", + "type": "string", + "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization." + }, + "detail": { + "title": "Detail", + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "source": { + "title": "Source", + "allOf": [ + { + "$ref": "#/components/schemas/ErrorSource" + } + ], + "description": "An object containing references to the source of the error" + }, + "meta": { + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/Meta" + } + ], + "description": "a meta object containing non-standard meta-information about the error." + } + }, + "description": "detail MUST be present" + }, "Provider": { "title": "Provider", "required": [ @@ -1589,127 +1710,6 @@ } }, "description": "OPTiMaDe-specific warning class based on OPTiMaDe-specific JSON API Error.\nFrom the specification:\n\n A warning resource object is defined similarly to a JSON API\n error object, but MUST also include the field type, which MUST\n have the value \"warning\". The field detail MUST be present and\n SHOULD contain a non-critical message, e.g., reporting\n unrecognized search attributes or deprecated features.\n\nNote: Must be named \"Warnings\", since \"Warning\" is a built-in Python class." - }, - "optimade__models__jsonapi__Error": { - "title": "Error", - "type": "object", - "properties": { - "id": { - "title": "Id", - "type": "string", - "description": "A unique identifier for this particular occurrence of the problem." - }, - "links": { - "title": "Links", - "allOf": [ - { - "$ref": "#/components/schemas/ErrorLinks" - } - ], - "description": "A links object storing about" - }, - "status": { - "title": "Status", - "type": "string", - "description": "the HTTP status code applicable to this problem, expressed as a string value." - }, - "code": { - "title": "Code", - "type": "string", - "description": "an application-specific error code, expressed as a string value." - }, - "title": { - "title": "Title", - "type": "string", - "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization." - }, - "detail": { - "title": "Detail", - "type": "string", - "description": "A human-readable explanation specific to this occurrence of the problem." - }, - "source": { - "title": "Source", - "allOf": [ - { - "$ref": "#/components/schemas/ErrorSource" - } - ], - "description": "An object containing references to the source of the error" - }, - "meta": { - "title": "Meta", - "allOf": [ - { - "$ref": "#/components/schemas/Meta" - } - ], - "description": "a meta object containing non-standard meta-information about the error." - } - }, - "description": "An error response" - }, - "optimade__models__optimade_json__Error": { - "title": "Error", - "required": [ - "detail" - ], - "type": "object", - "properties": { - "id": { - "title": "Id", - "type": "string", - "description": "A unique identifier for this particular occurrence of the problem." - }, - "links": { - "title": "Links", - "allOf": [ - { - "$ref": "#/components/schemas/ErrorLinks" - } - ], - "description": "A links object storing about" - }, - "status": { - "title": "Status", - "type": "string", - "description": "the HTTP status code applicable to this problem, expressed as a string value." - }, - "code": { - "title": "Code", - "type": "string", - "description": "an application-specific error code, expressed as a string value." - }, - "title": { - "title": "Title", - "type": "string", - "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization." - }, - "detail": { - "title": "Detail", - "type": "string", - "description": "A human-readable explanation specific to this occurrence of the problem." - }, - "source": { - "title": "Source", - "allOf": [ - { - "$ref": "#/components/schemas/ErrorSource" - } - ], - "description": "An object containing references to the source of the error" - }, - "meta": { - "title": "Meta", - "allOf": [ - { - "$ref": "#/components/schemas/Meta" - } - ], - "description": "a meta object containing non-standard meta-information about the error." - } - }, - "description": "detail MUST be present" } } } diff --git a/openapi/openapi.json b/openapi/openapi.json index 1def740de..e29fa550f 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -1171,7 +1171,7 @@ "uniqueItems": true, "type": "array", "items": { - "$ref": "#/components/schemas/optimade__models__jsonapi__Error" + "$ref": "#/components/schemas/Error" }, "description": "A list of errors" }, @@ -1309,6 +1309,65 @@ }, "description": "Contains key-value pairs representing the entry's properties." }, + "Error": { + "title": "Error", + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string", + "description": "A unique identifier for this particular occurrence of the problem." + }, + "links": { + "title": "Links", + "allOf": [ + { + "$ref": "#/components/schemas/ErrorLinks" + } + ], + "description": "A links object storing about" + }, + "status": { + "title": "Status", + "type": "string", + "description": "the HTTP status code applicable to this problem, expressed as a string value." + }, + "code": { + "title": "Code", + "type": "string", + "description": "an application-specific error code, expressed as a string value." + }, + "title": { + "title": "Title", + "type": "string", + "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization." + }, + "detail": { + "title": "Detail", + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "source": { + "title": "Source", + "allOf": [ + { + "$ref": "#/components/schemas/ErrorSource" + } + ], + "description": "An object containing references to the source of the error" + }, + "meta": { + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/Meta" + } + ], + "description": "a meta object containing non-standard meta-information about the error." + } + }, + "description": "An error response" + }, "ErrorLinks": { "title": "ErrorLinks", "type": "object", @@ -1369,7 +1428,7 @@ "title": "Errors", "type": "array", "items": { - "$ref": "#/components/schemas/optimade__models__optimade_json__Error" + "$ref": "#/components/schemas/OptimadeError" } }, "included": { @@ -1500,7 +1559,7 @@ "uniqueItems": true, "type": "array", "items": { - "$ref": "#/components/schemas/optimade__models__jsonapi__Error" + "$ref": "#/components/schemas/Error" }, "description": "A list of errors" }, @@ -1734,7 +1793,7 @@ "uniqueItems": true, "type": "array", "items": { - "$ref": "#/components/schemas/optimade__models__jsonapi__Error" + "$ref": "#/components/schemas/Error" }, "description": "A list of errors" }, @@ -1782,6 +1841,68 @@ "properties": {}, "description": "Non-standard meta-information that can not be represented as an attribute or relationship." }, + "OptimadeError": { + "title": "OptimadeError", + "required": [ + "detail" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string", + "description": "A unique identifier for this particular occurrence of the problem." + }, + "links": { + "title": "Links", + "allOf": [ + { + "$ref": "#/components/schemas/ErrorLinks" + } + ], + "description": "A links object storing about" + }, + "status": { + "title": "Status", + "type": "string", + "description": "the HTTP status code applicable to this problem, expressed as a string value." + }, + "code": { + "title": "Code", + "type": "string", + "description": "an application-specific error code, expressed as a string value." + }, + "title": { + "title": "Title", + "type": "string", + "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization." + }, + "detail": { + "title": "Detail", + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "source": { + "title": "Source", + "allOf": [ + { + "$ref": "#/components/schemas/ErrorSource" + } + ], + "description": "An object containing references to the source of the error" + }, + "meta": { + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/Meta" + } + ], + "description": "a meta object containing non-standard meta-information about the error." + } + }, + "description": "detail MUST be present" + }, "Person": { "title": "Person", "required": [ @@ -2157,7 +2278,7 @@ "uniqueItems": true, "type": "array", "items": { - "$ref": "#/components/schemas/optimade__models__jsonapi__Error" + "$ref": "#/components/schemas/Error" }, "description": "A list of errors" }, @@ -2225,7 +2346,7 @@ "uniqueItems": true, "type": "array", "items": { - "$ref": "#/components/schemas/optimade__models__jsonapi__Error" + "$ref": "#/components/schemas/Error" }, "description": "A list of errors" }, @@ -2879,7 +3000,7 @@ "uniqueItems": true, "type": "array", "items": { - "$ref": "#/components/schemas/optimade__models__jsonapi__Error" + "$ref": "#/components/schemas/Error" }, "description": "A list of errors" }, @@ -2947,7 +3068,7 @@ "uniqueItems": true, "type": "array", "items": { - "$ref": "#/components/schemas/optimade__models__jsonapi__Error" + "$ref": "#/components/schemas/Error" }, "description": "A list of errors" }, @@ -3158,127 +3279,6 @@ } }, "description": "OPTiMaDe-specific warning class based on OPTiMaDe-specific JSON API Error.\nFrom the specification:\n\n A warning resource object is defined similarly to a JSON API\n error object, but MUST also include the field type, which MUST\n have the value \"warning\". The field detail MUST be present and\n SHOULD contain a non-critical message, e.g., reporting\n unrecognized search attributes or deprecated features.\n\nNote: Must be named \"Warnings\", since \"Warning\" is a built-in Python class." - }, - "optimade__models__jsonapi__Error": { - "title": "Error", - "type": "object", - "properties": { - "id": { - "title": "Id", - "type": "string", - "description": "A unique identifier for this particular occurrence of the problem." - }, - "links": { - "title": "Links", - "allOf": [ - { - "$ref": "#/components/schemas/ErrorLinks" - } - ], - "description": "A links object storing about" - }, - "status": { - "title": "Status", - "type": "string", - "description": "the HTTP status code applicable to this problem, expressed as a string value." - }, - "code": { - "title": "Code", - "type": "string", - "description": "an application-specific error code, expressed as a string value." - }, - "title": { - "title": "Title", - "type": "string", - "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization." - }, - "detail": { - "title": "Detail", - "type": "string", - "description": "A human-readable explanation specific to this occurrence of the problem." - }, - "source": { - "title": "Source", - "allOf": [ - { - "$ref": "#/components/schemas/ErrorSource" - } - ], - "description": "An object containing references to the source of the error" - }, - "meta": { - "title": "Meta", - "allOf": [ - { - "$ref": "#/components/schemas/Meta" - } - ], - "description": "a meta object containing non-standard meta-information about the error." - } - }, - "description": "An error response" - }, - "optimade__models__optimade_json__Error": { - "title": "Error", - "required": [ - "detail" - ], - "type": "object", - "properties": { - "id": { - "title": "Id", - "type": "string", - "description": "A unique identifier for this particular occurrence of the problem." - }, - "links": { - "title": "Links", - "allOf": [ - { - "$ref": "#/components/schemas/ErrorLinks" - } - ], - "description": "A links object storing about" - }, - "status": { - "title": "Status", - "type": "string", - "description": "the HTTP status code applicable to this problem, expressed as a string value." - }, - "code": { - "title": "Code", - "type": "string", - "description": "an application-specific error code, expressed as a string value." - }, - "title": { - "title": "Title", - "type": "string", - "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization." - }, - "detail": { - "title": "Detail", - "type": "string", - "description": "A human-readable explanation specific to this occurrence of the problem." - }, - "source": { - "title": "Source", - "allOf": [ - { - "$ref": "#/components/schemas/ErrorSource" - } - ], - "description": "An object containing references to the source of the error" - }, - "meta": { - "title": "Meta", - "allOf": [ - { - "$ref": "#/components/schemas/Meta" - } - ], - "description": "a meta object containing non-standard meta-information about the error." - } - }, - "description": "detail MUST be present" } } } diff --git a/optimade/models/optimade_json.py b/optimade/models/optimade_json.py index 6ca2dd3e1..51c64cebb 100644 --- a/optimade/models/optimade_json.py +++ b/optimade/models/optimade_json.py @@ -7,7 +7,7 @@ __all__ = ( - "Error", + "OptimadeError", "Failure", "Success", "Warnings", @@ -17,7 +17,7 @@ ) -class Error(jsonapi.Error): +class OptimadeError(jsonapi.Error): """detail MUST be present""" detail: str = Field( @@ -33,7 +33,7 @@ class Failure(jsonapi.Response): None, description="A meta object containing non-standard information related to the Success", ) - errors: Set[Error] = Field( + errors: Set[OptimadeError] = Field( ..., description="A list of OPTiMaDe-specific JSON API error objects, where the field detail MUST be present.", ) @@ -75,7 +75,7 @@ def either_data_meta_or_errors_must_be_set(cls, values): return values -class Warnings(Error): +class Warnings(OptimadeError): """OPTiMaDe-specific warning class based on OPTiMaDe-specific JSON API Error. From the specification: diff --git a/optimade/models/toplevel.py b/optimade/models/toplevel.py index 0feb770a7..25b1547b8 100644 --- a/optimade/models/toplevel.py +++ b/optimade/models/toplevel.py @@ -13,7 +13,7 @@ from .entries import EntryInfoResource, EntryResource from .index_metadb import IndexInfoResource from .links import LinksResource -from .optimade_json import Error, Success, Failure, Warnings +from .optimade_json import OptimadeError, Success, Failure, Warnings from .references import ReferenceResource from .structures import StructureResource @@ -173,7 +173,7 @@ class ResponseMeta(Meta): class ErrorResponse(Failure): meta: Optional[ResponseMeta] = Field(None) - errors: List[Error] = Field(...) + errors: List[OptimadeError] = Field(...) class IndexInfoResponse(Success): diff --git a/optimade/server/exception_handlers.py b/optimade/server/exception_handlers.py index 25e901820..2d3a36976 100644 --- a/optimade/server/exception_handlers.py +++ b/optimade/server/exception_handlers.py @@ -10,7 +10,7 @@ from starlette.requests import Request from starlette.responses import JSONResponse -from optimade.models import Error, ErrorResponse, ErrorSource +from optimade.models import OptimadeError, ErrorResponse, ErrorSource from .config import CONFIG from .routers.utils import meta_values @@ -38,7 +38,7 @@ def general_exception( errors = kwargs.get("errors", None) if not errors: - errors = [Error(detail=detail, status=status_code, title=title)] + errors = [OptimadeError(detail=detail, status=status_code, title=title)] try: response = ErrorResponse( @@ -82,7 +82,9 @@ def validation_exception_handler(request: Request, exc: ValidationError): code = error["type"] detail = error["msg"] errors.append( - Error(detail=detail, status=status, title=title, source=source, code=code) + OptimadeError( + detail=detail, status=status, title=title, source=source, code=code + ) ) return general_exception(request, exc, status_code=status, errors=errors) @@ -97,7 +99,7 @@ def grammar_not_implemented_handler(request: Request, exc: VisitError): if not str(exc.orig_exc) else str(exc.orig_exc) ) - error = Error(detail=detail, status=status, title=title) + error = OptimadeError(detail=detail, status=status, title=title) return general_exception(request, exc, status_code=status, errors=[error]) From 1e72305fb8acc2b74335c85a23b6284eae401e23 Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen Date: Thu, 27 Feb 2020 18:20:34 +0100 Subject: [PATCH 2/6] Better use of Error model --- openapi/index_openapi.json | 4 +++- openapi/openapi.json | 4 +++- optimade/models/optimade_json.py | 2 +- optimade/models/toplevel.py | 3 +-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openapi/index_openapi.json b/openapi/index_openapi.json index 6dfa66746..da8ea1c48 100644 --- a/openapi/index_openapi.json +++ b/openapi/index_openapi.json @@ -528,10 +528,12 @@ }, "errors": { "title": "Errors", + "uniqueItems": true, "type": "array", "items": { "$ref": "#/components/schemas/OptimadeError" - } + }, + "description": "A list of OPTiMaDe-specific JSON API error objects, where the field detail MUST be present." }, "included": { "title": "Included", diff --git a/openapi/openapi.json b/openapi/openapi.json index e29fa550f..4cbfe2088 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -1426,10 +1426,12 @@ }, "errors": { "title": "Errors", + "uniqueItems": true, "type": "array", "items": { "$ref": "#/components/schemas/OptimadeError" - } + }, + "description": "A list of OPTiMaDe-specific JSON API error objects, where the field detail MUST be present." }, "included": { "title": "Included", diff --git a/optimade/models/optimade_json.py b/optimade/models/optimade_json.py index 51c64cebb..5d001e186 100644 --- a/optimade/models/optimade_json.py +++ b/optimade/models/optimade_json.py @@ -1,5 +1,5 @@ """Modified JSON API v1.0 for OPTiMaDe API""" -# pylint: disable=no-self-argument +# pylint: disable=no-self-argument,no-name-in-module from pydantic import Field, root_validator, BaseModel from typing import Optional, Set, Union, List diff --git a/optimade/models/toplevel.py b/optimade/models/toplevel.py index 25b1547b8..0009581c4 100644 --- a/optimade/models/toplevel.py +++ b/optimade/models/toplevel.py @@ -13,7 +13,7 @@ from .entries import EntryInfoResource, EntryResource from .index_metadb import IndexInfoResource from .links import LinksResource -from .optimade_json import OptimadeError, Success, Failure, Warnings +from .optimade_json import Success, Failure, Warnings from .references import ReferenceResource from .structures import StructureResource @@ -173,7 +173,6 @@ class ResponseMeta(Meta): class ErrorResponse(Failure): meta: Optional[ResponseMeta] = Field(None) - errors: List[OptimadeError] = Field(...) class IndexInfoResponse(Success): From 45982616a23873c220136965e6b2ba51189a61b2 Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen Date: Thu, 27 Feb 2020 18:49:27 +0100 Subject: [PATCH 3/6] Fix mistyping of BaseRelationshipMeta --- openapi/index_openapi.json | 6 +++--- openapi/openapi.json | 6 +++--- optimade/models/jsonapi.py | 9 +++++++-- optimade/models/optimade_json.py | 8 ++++---- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/openapi/index_openapi.json b/openapi/index_openapi.json index da8ea1c48..f82d307c7 100644 --- a/openapi/index_openapi.json +++ b/openapi/index_openapi.json @@ -262,8 +262,8 @@ }, "description": "A JSON object containing information about an available API version" }, - "BaseRealationshipMeta": { - "title": "BaseRealationshipMeta", + "BaseRelationshipMeta": { + "title": "BaseRelationshipMeta", "required": [ "description" ], @@ -299,7 +299,7 @@ "title": "Meta", "allOf": [ { - "$ref": "#/components/schemas/BaseRealationshipMeta" + "$ref": "#/components/schemas/BaseRelationshipMeta" } ], "description": "Relationship meta field. MUST contain 'description' if supplied." diff --git a/openapi/openapi.json b/openapi/openapi.json index 4cbfe2088..39b91d406 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -1040,8 +1040,8 @@ }, "description": "Resource objects appear in a JSON:API document to represent resources." }, - "BaseRealationshipMeta": { - "title": "BaseRealationshipMeta", + "BaseRelationshipMeta": { + "title": "BaseRelationshipMeta", "required": [ "description" ], @@ -1077,7 +1077,7 @@ "title": "Meta", "allOf": [ { - "$ref": "#/components/schemas/BaseRealationshipMeta" + "$ref": "#/components/schemas/BaseRelationshipMeta" } ], "description": "Relationship meta field. MUST contain 'description' if supplied." diff --git a/optimade/models/jsonapi.py b/optimade/models/jsonapi.py index 6e7f64b3d..2631b3ad0 100644 --- a/optimade/models/jsonapi.py +++ b/optimade/models/jsonapi.py @@ -1,7 +1,12 @@ """This module should reproduce JSON API v1.0 https://jsonapi.org/format/1.0/""" # pylint: disable=no-self-argument -from typing import Optional, Set, Union, List -from pydantic import BaseModel, AnyUrl, Field, root_validator +from typing import Optional, Union, List, Set +from pydantic import ( # pylint: disable=no-name-in-module + BaseModel, + AnyUrl, + Field, + root_validator, +) __all__ = ( diff --git a/optimade/models/optimade_json.py b/optimade/models/optimade_json.py index 5d001e186..550d9ad74 100644 --- a/optimade/models/optimade_json.py +++ b/optimade/models/optimade_json.py @@ -1,7 +1,7 @@ """Modified JSON API v1.0 for OPTiMaDe API""" # pylint: disable=no-self-argument,no-name-in-module from pydantic import Field, root_validator, BaseModel -from typing import Optional, Set, Union, List +from typing import Optional, Union, List, Set from . import jsonapi @@ -11,7 +11,7 @@ "Failure", "Success", "Warnings", - "BaseRealationshipMeta", + "BaseRelationshipMeta", "BaseRelationshipResource", "Relationship", ) @@ -99,7 +99,7 @@ def status_must_not_be_specified(cls, values): return values -class BaseRealationshipMeta(BaseModel): +class BaseRelationshipMeta(BaseModel): """Specific meta field for base relationship resource""" description: str = Field( @@ -110,7 +110,7 @@ class BaseRealationshipMeta(BaseModel): class BaseRelationshipResource(jsonapi.BaseResource): """Minimum requirements to represent a relationship resource""" - meta: Optional[BaseRealationshipMeta] = Field( + meta: Optional[BaseRelationshipMeta] = Field( None, description="Relationship meta field. MUST contain 'description' if supplied.", ) From 9b2845be590b86acf481a35f26dac713d7ecc514 Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen Date: Fri, 28 Feb 2020 15:01:35 +0100 Subject: [PATCH 4/6] Rename toplevel to responses Moved all non-response models/classes to optimade_json. This mainly includes Warnings and ResponseMeta and all their used/internal models. --- openapi/index_openapi.json | 6 +- openapi/openapi.json | 16 +-- optimade/models/__init__.py | 4 +- optimade/models/jsonapi.py | 2 +- optimade/models/optimade_json.py | 204 +++++++++++++++++++++++----- optimade/models/responses.py | 77 +++++++++++ optimade/models/toplevel.py | 222 ------------------------------- 7 files changed, 260 insertions(+), 271 deletions(-) create mode 100644 optimade/models/responses.py delete mode 100644 optimade/models/toplevel.py diff --git a/openapi/index_openapi.json b/openapi/index_openapi.json index f82d307c7..c2629d3fd 100644 --- a/openapi/index_openapi.json +++ b/openapi/index_openapi.json @@ -551,7 +551,7 @@ "$ref": "#/components/schemas/ToplevelLinks" } ], - "description": "Links associated with the primary data" + "description": "Links associated with the primary data or errors" }, "jsonapi": { "title": "Jsonapi", @@ -792,7 +792,7 @@ "$ref": "#/components/schemas/ToplevelLinks" } ], - "description": "Links associated with the primary data" + "description": "Links associated with the primary data or errors" }, "jsonapi": { "title": "Jsonapi", @@ -1053,7 +1053,7 @@ "$ref": "#/components/schemas/ToplevelLinks" } ], - "description": "Links associated with the primary data" + "description": "Links associated with the primary data or errors" }, "jsonapi": { "title": "Jsonapi", diff --git a/openapi/openapi.json b/openapi/openapi.json index 39b91d406..10fa3bbee 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -1191,7 +1191,7 @@ "$ref": "#/components/schemas/ToplevelLinks" } ], - "description": "Links associated with the primary data" + "description": "Links associated with the primary data or errors" }, "jsonapi": { "title": "Jsonapi", @@ -1449,7 +1449,7 @@ "$ref": "#/components/schemas/ToplevelLinks" } ], - "description": "Links associated with the primary data" + "description": "Links associated with the primary data or errors" }, "jsonapi": { "title": "Jsonapi", @@ -1581,7 +1581,7 @@ "$ref": "#/components/schemas/ToplevelLinks" } ], - "description": "Links associated with the primary data" + "description": "Links associated with the primary data or errors" }, "jsonapi": { "title": "Jsonapi", @@ -1823,7 +1823,7 @@ "$ref": "#/components/schemas/ToplevelLinks" } ], - "description": "Links associated with the primary data" + "description": "Links associated with the primary data or errors" }, "jsonapi": { "title": "Jsonapi", @@ -2308,7 +2308,7 @@ "$ref": "#/components/schemas/ToplevelLinks" } ], - "description": "Links associated with the primary data" + "description": "Links associated with the primary data or errors" }, "jsonapi": { "title": "Jsonapi", @@ -2376,7 +2376,7 @@ "$ref": "#/components/schemas/ToplevelLinks" } ], - "description": "Links associated with the primary data" + "description": "Links associated with the primary data or errors" }, "jsonapi": { "title": "Jsonapi", @@ -3030,7 +3030,7 @@ "$ref": "#/components/schemas/ToplevelLinks" } ], - "description": "Links associated with the primary data" + "description": "Links associated with the primary data or errors" }, "jsonapi": { "title": "Jsonapi", @@ -3098,7 +3098,7 @@ "$ref": "#/components/schemas/ToplevelLinks" } ], - "description": "Links associated with the primary data" + "description": "Links associated with the primary data or errors" }, "jsonapi": { "title": "Jsonapi", diff --git a/optimade/models/__init__.py b/optimade/models/__init__.py index f522d503e..d0fb7bbf4 100644 --- a/optimade/models/__init__.py +++ b/optimade/models/__init__.py @@ -8,8 +8,8 @@ from .links import * from .optimade_json import * from .references import * +from .responses import * from .structures import * -from .toplevel import * __all__ = ( jsonapi.__all__ # noqa @@ -20,6 +20,6 @@ + links.__all__ # noqa + optimade_json.__all__ # noqa + references.__all__ # noqa + + responses.__all__ # noqa + structures.__all__ # noqa - + toplevel.__all__ # noqa ) diff --git a/optimade/models/jsonapi.py b/optimade/models/jsonapi.py index 2631b3ad0..adf05ec0a 100644 --- a/optimade/models/jsonapi.py +++ b/optimade/models/jsonapi.py @@ -267,7 +267,7 @@ class Response(BaseModel): None, description="A list of resources that are included" ) links: Optional[ToplevelLinks] = Field( - None, description="Links associated with the failure" + None, description="Links associated with the primary data or errors" ) jsonapi: Optional[JsonApi] = Field( None, description="Information about the JSON API used" diff --git a/optimade/models/optimade_json.py b/optimade/models/optimade_json.py index 550d9ad74..04999b61c 100644 --- a/optimade/models/optimade_json.py +++ b/optimade/models/optimade_json.py @@ -1,12 +1,19 @@ """Modified JSON API v1.0 for OPTiMaDe API""" # pylint: disable=no-self-argument,no-name-in-module -from pydantic import Field, root_validator, BaseModel +from pydantic import Field, root_validator, BaseModel, AnyHttpUrl, EmailStr from typing import Optional, Union, List, Set +from datetime import datetime + from . import jsonapi __all__ = ( + "ResponseMetaQuery", + "Provider", + "ImplementationMaintainer", + "Implementation", + "ResponseMeta", "OptimadeError", "Failure", "Success", @@ -26,10 +33,167 @@ class OptimadeError(jsonapi.Error): ) +class Warnings(OptimadeError): + """OPTiMaDe-specific warning class based on OPTiMaDe-specific JSON API Error. + From the specification: + + A warning resource object is defined similarly to a JSON API + error object, but MUST also include the field type, which MUST + have the value "warning". The field detail MUST be present and + SHOULD contain a non-critical message, e.g., reporting + unrecognized search attributes or deprecated features. + + Note: Must be named "Warnings", since "Warning" is a built-in Python class. + """ + + type: str = Field( + "warning", const=True, description='Warnings must be of type "warning"' + ) + + @root_validator(pre=True) + def status_must_not_be_specified(cls, values): + if values.get("status", None) is not None: + raise ValueError("status MUST NOT be specified for warnings") + return values + + +class ResponseMetaQuery(BaseModel): + """ Information on the query that was requested. """ + + representation: str = Field( + ..., + description="a string with the part of the URL that follows the base URL. Example: '/structures?'", + ) + + +class Provider(BaseModel): + """Information on the database provider of the implementation.""" + + name: str = Field(..., description="a short name for the database provider") + + description: str = Field( + ..., description="a longer description of the database provider" + ) + + prefix: str = Field( + ..., description="database-provider-specific prefix as found in " "Appendix 1." + ) + + homepage: Optional[Union[AnyHttpUrl, jsonapi.Link]] = Field( + None, + description="a [JSON API links object](http://jsonapi.org/format/1.0#document-links) " + "pointing to homepage of the database provider, either " + "directly as a string, or as a link object.", + ) + + index_base_url: Optional[Union[AnyHttpUrl, jsonapi.Link]] = Field( + None, + description="a [JSON API links object](http://jsonapi.org/format/1.0#document-links) " + "pointing to the base URL for the `index` meta-database as " + "specified in Appendix 1, either directly as a string, or " + "as a link object.", + ) + + +class ImplementationMaintainer(BaseModel): + """Details about the maintainer of the implementation""" + + email: EmailStr = Field(..., description="the maintainer's email address") + + +class Implementation(BaseModel): + """Information on the server implementation""" + + name: Optional[str] = Field(None, description="name of the implementation") + + version: Optional[str] = Field( + None, description="version string of the current implementation" + ) + + source_url: Optional[AnyHttpUrl] = Field( + None, + description="URL of the implementation source, either downloadable archive or version control system", + ) + + maintainer: Optional[ImplementationMaintainer] = Field( + None, + description="A dictionary providing details about the maintainer of the implementation.", + ) + + +class ResponseMeta(jsonapi.Meta): + """ + A [JSON API meta member](https://jsonapi.org/format/1.0#document-meta) + that contains JSON API meta objects of non-standard + meta-information. + + OPTIONAL additional information global to the query that is not + specified in this document, MUST start with a + database-provider-specific prefix. + """ + + query: ResponseMetaQuery = Field( + ..., description="information on the query that was requested" + ) + + api_version: str = Field( + ..., + description="a string containing the version of the API " + "implementation, e.g. v0.9.5", + ) + + time_stamp: datetime = Field( + ..., + description="a string containing the date and time at which the query was exexcuted", + ) + + data_returned: int = Field( + ..., + description="an integer containing the number of data objects " + "returned for the query.", + ge=0, + ) + + more_data_available: bool = Field( + ..., description="`false` if all data has been returned, and `true` " "if not." + ) + + provider: Provider = Field( + ..., description="information on the database provider of the implementation." + ) + + data_available: Optional[int] = Field( + None, + description="an integer containing the total number of data " + "objects available in the database", + ) + + last_id: Optional[str] = Field( + None, description="a string containing the last ID returned" + ) + + response_message: Optional[str] = Field( + None, description="response string from the server" + ) + + implementation: Optional[Implementation] = Field( + None, description="a dictionary describing the server implementation" + ) + + warnings: Optional[List[Warnings]] = Field( + None, + description="List of warning resource objects representing non-critical errors or warnings. " + "A warning resource object is defined similarly to a JSON API error object, but MUST also include the field type, " + 'which MUST have the value "warning". The field detail MUST be present and SHOULD contain a non-critical message, ' + "e.g., reporting unrecognized search attributes or deprecated features. The field status, representing a HTTP " + "response status code, MUST NOT be present for a warning resource object. This is an exclusive field for error resource objects.", + ) + + class Failure(jsonapi.Response): """errors MUST be present and data MUST be skipped""" - meta: Optional[jsonapi.Meta] = Field( + meta: Optional[ResponseMeta] = Field( None, description="A meta object containing non-standard information related to the Success", ) @@ -37,9 +201,6 @@ class Failure(jsonapi.Response): ..., description="A list of OPTiMaDe-specific JSON API error objects, where the field detail MUST be present.", ) - links: Optional[jsonapi.ToplevelLinks] = Field( - None, description="Links associated with the primary data" - ) @root_validator(pre=True) def data_must_be_skipped(cls, values): @@ -51,17 +212,14 @@ def data_must_be_skipped(cls, values): class Success(jsonapi.Response): """errors are not allowed""" - meta: Optional[jsonapi.Meta] = Field( + meta: Optional[ResponseMeta] = Field( None, description="A meta object containing non-standard information related to the Success", ) - links: Optional[jsonapi.ToplevelLinks] = Field( - None, description="Links associated with the primary data" - ) @root_validator(pre=True) def either_data_meta_or_errors_must_be_set(cls, values): - """Overwriting the existing validation function""" + """Overwriting the existing validation function, since 'errors' MUST NOT be set""" required_fields = ("data", "meta") if not any(values.get(field) for field in required_fields): raise ValueError( @@ -75,31 +233,7 @@ def either_data_meta_or_errors_must_be_set(cls, values): return values -class Warnings(OptimadeError): - """OPTiMaDe-specific warning class based on OPTiMaDe-specific JSON API Error. - From the specification: - - A warning resource object is defined similarly to a JSON API - error object, but MUST also include the field type, which MUST - have the value "warning". The field detail MUST be present and - SHOULD contain a non-critical message, e.g., reporting - unrecognized search attributes or deprecated features. - - Note: Must be named "Warnings", since "Warning" is a built-in Python class. - """ - - type: str = Field( - "warning", const=True, description='Warnings must be of type "warning"' - ) - - @root_validator(pre=True) - def status_must_not_be_specified(cls, values): - if values.get("status", None) is not None: - raise ValueError("status MUST NOT be specified for warnings") - return values - - -class BaseRelationshipMeta(BaseModel): +class BaseRelationshipMeta(jsonapi.Meta): """Specific meta field for base relationship resource""" description: str = Field( diff --git a/optimade/models/responses.py b/optimade/models/responses.py new file mode 100644 index 000000000..55f0ad863 --- /dev/null +++ b/optimade/models/responses.py @@ -0,0 +1,77 @@ +from typing import Union, List, Optional, Dict, Any + +from pydantic import Field + +from .baseinfo import BaseInfoResource +from .entries import EntryInfoResource, EntryResource +from .index_metadb import IndexInfoResource +from .links import LinksResource +from .optimade_json import Success, Failure, ResponseMeta +from .references import ReferenceResource +from .structures import StructureResource + + +__all__ = ( + "ErrorResponse", + "EntryInfoResponse", + "IndexInfoResponse", + "InfoResponse", + "LinksResponse", + "EntryResponseOne", + "EntryResponseMany", + "StructureResponseOne", + "StructureResponseMany", + "ReferenceResponseOne", + "ReferenceResponseMany", +) + + +class ErrorResponse(Failure): + meta: Optional[ResponseMeta] = Field(None) + + +class IndexInfoResponse(Success): + meta: Optional[ResponseMeta] = Field(None) + data: IndexInfoResource = Field(...) + + +class EntryInfoResponse(Success): + meta: Optional[ResponseMeta] = Field(None) + data: EntryInfoResource = Field(...) + + +class InfoResponse(Success): + meta: Optional[ResponseMeta] = Field(None) + data: BaseInfoResource = Field(...) + + +class EntryResponseOne(Success): + meta: Optional[ResponseMeta] = Field(None) + data: Union[EntryResource, Dict[str, Any], None] = Field(...) + included: Optional[Union[List[EntryResource], List[Dict[str, Any]]]] = Field(None) + + +class EntryResponseMany(Success): + meta: Optional[ResponseMeta] = Field(None) + data: Union[List[EntryResource], List[Dict[str, Any]]] = Field(...) + included: Optional[Union[List[EntryResource], List[Dict[str, Any]]]] = Field(None) + + +class LinksResponse(EntryResponseMany): + data: Union[List[LinksResource], List[Dict[str, Any]]] = Field(...) + + +class StructureResponseOne(EntryResponseOne): + data: Union[StructureResource, Dict[str, Any], None] = Field(...) + + +class StructureResponseMany(EntryResponseMany): + data: Union[List[StructureResource], List[Dict[str, Any]]] = Field(...) + + +class ReferenceResponseOne(EntryResponseOne): + data: Union[ReferenceResource, Dict[str, Any], None] = Field(...) + + +class ReferenceResponseMany(EntryResponseMany): + data: Union[List[ReferenceResource], List[Dict[str, Any]]] = Field(...) diff --git a/optimade/models/toplevel.py b/optimade/models/toplevel.py deleted file mode 100644 index 0009581c4..000000000 --- a/optimade/models/toplevel.py +++ /dev/null @@ -1,222 +0,0 @@ -from datetime import datetime -from typing import Union, List, Optional, Dict, Any - -from pydantic import ( # pylint: disable=no-name-in-module - BaseModel, - AnyHttpUrl, - Field, - EmailStr, -) - -from .jsonapi import Link, Meta -from .baseinfo import BaseInfoResource -from .entries import EntryInfoResource, EntryResource -from .index_metadb import IndexInfoResource -from .links import LinksResource -from .optimade_json import Success, Failure, Warnings -from .references import ReferenceResource -from .structures import StructureResource - - -__all__ = ( - "ResponseMetaQuery", - "Provider", - "ImplementationMaintainer", - "Implementation", - "ResponseMeta", - "ErrorResponse", - "EntryInfoResponse", - "IndexInfoResponse", - "InfoResponse", - "LinksResponse", - "EntryResponseOne", - "EntryResponseMany", - "StructureResponseOne", - "StructureResponseMany", - "ReferenceResponseOne", - "ReferenceResponseMany", -) - - -class ResponseMetaQuery(BaseModel): - """ Information on the query that was requested. """ - - representation: str = Field( - ..., - description="a string with the part of the URL that follows the base URL. Example: '/structures?'", - ) - - -class Provider(BaseModel): - """Information on the database provider of the implementation.""" - - name: str = Field(..., description="a short name for the database provider") - - description: str = Field( - ..., description="a longer description of the database provider" - ) - - prefix: str = Field( - ..., description="database-provider-specific prefix as found in " "Appendix 1." - ) - - homepage: Optional[Union[AnyHttpUrl, Link]] = Field( - None, - description="a [JSON API links object](http://jsonapi.org/format/1.0#document-links) " - "pointing to homepage of the database provider, either " - "directly as a string, or as a link object.", - ) - - index_base_url: Optional[Union[AnyHttpUrl, Link]] = Field( - None, - description="a [JSON API links object](http://jsonapi.org/format/1.0#document-links) " - "pointing to the base URL for the `index` meta-database as " - "specified in Appendix 1, either directly as a string, or " - "as a link object.", - ) - - -class ImplementationMaintainer(BaseModel): - """Details about the maintainer of the implementation""" - - email: EmailStr = Field(..., description="the maintainer's email address") - - -class Implementation(BaseModel): - """Information on the server implementation""" - - name: Optional[str] = Field(None, description="name of the implementation") - - version: Optional[str] = Field( - None, description="version string of the current implementation" - ) - - source_url: Optional[AnyHttpUrl] = Field( - None, - description="URL of the implementation source, either downloadable archive or version control system", - ) - - maintainer: Optional[ImplementationMaintainer] = Field( - None, - description="A dictionary providing details about the maintainer of the implementation.", - ) - - -class ResponseMeta(Meta): - """ - A [JSON API meta member](https://jsonapi.org/format/1.0#document-meta) - that contains JSON API meta objects of non-standard - meta-information. - - OPTIONAL additional information global to the query that is not - specified in this document, MUST start with a - database-provider-specific prefix. - """ - - query: ResponseMetaQuery = Field( - ..., description="information on the query that was requested" - ) - - api_version: str = Field( - ..., - description="a string containing the version of the API " - "implementation, e.g. v0.9.5", - ) - - time_stamp: datetime = Field( - ..., - description="a string containing the date and time at which the query was exexcuted", - ) - - data_returned: int = Field( - ..., - description="an integer containing the number of data objects " - "returned for the query.", - ge=0, - ) - - more_data_available: bool = Field( - ..., description="`false` if all data has been returned, and `true` " "if not." - ) - - provider: Provider = Field( - ..., description="information on the database provider of the implementation." - ) - - data_available: Optional[int] = Field( - None, - description="an integer containing the total number of data " - "objects available in the database", - ) - - last_id: Optional[str] = Field( - None, description="a string containing the last ID returned" - ) - - response_message: Optional[str] = Field( - None, description="response string from the server" - ) - - implementation: Optional[Implementation] = Field( - None, description="a dictionary describing the server implementation" - ) - - warnings: Optional[List[Warnings]] = Field( - None, - description="List of warning resource objects representing non-critical errors or warnings. " - "A warning resource object is defined similarly to a JSON API error object, but MUST also include the field type, " - 'which MUST have the value "warning". The field detail MUST be present and SHOULD contain a non-critical message, ' - "e.g., reporting unrecognized search attributes or deprecated features. The field status, representing a HTTP " - "response status code, MUST NOT be present for a warning resource object. This is an exclusive field for error resource objects.", - ) - - -class ErrorResponse(Failure): - meta: Optional[ResponseMeta] = Field(None) - - -class IndexInfoResponse(Success): - meta: Optional[ResponseMeta] = Field(None) - data: IndexInfoResource = Field(...) - - -class EntryInfoResponse(Success): - meta: Optional[ResponseMeta] = Field(None) - data: EntryInfoResource = Field(...) - - -class InfoResponse(Success): - meta: Optional[ResponseMeta] = Field(None) - data: BaseInfoResource = Field(...) - - -class EntryResponseOne(Success): - meta: Optional[ResponseMeta] = Field(None) - data: Union[EntryResource, Dict[str, Any], None] = Field(...) - included: Optional[Union[List[EntryResource], List[Dict[str, Any]]]] = Field(None) - - -class EntryResponseMany(Success): - meta: Optional[ResponseMeta] = Field(None) - data: Union[List[EntryResource], List[Dict[str, Any]]] = Field(...) - included: Optional[Union[List[EntryResource], List[Dict[str, Any]]]] = Field(None) - - -class LinksResponse(EntryResponseMany): - data: Union[List[LinksResource], List[Dict[str, Any]]] = Field(...) - - -class StructureResponseOne(EntryResponseOne): - data: Union[StructureResource, Dict[str, Any], None] = Field(...) - - -class StructureResponseMany(EntryResponseMany): - data: Union[List[StructureResource], List[Dict[str, Any]]] = Field(...) - - -class ReferenceResponseOne(EntryResponseOne): - data: Union[ReferenceResource, Dict[str, Any], None] = Field(...) - - -class ReferenceResponseMany(EntryResponseMany): - data: Union[List[ReferenceResource], List[Dict[str, Any]]] = Field(...) From 245b66d4265b7f3858f0c2e2e9c5de217cdfdf41 Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen Date: Fri, 28 Feb 2020 16:09:41 +0100 Subject: [PATCH 5/6] Use List instead of Set in all models For the attributes/fields that should indeed have unique items, the OpenAPI attribute `uniqueItems` is manually set to `True`. Otherwise, the implementation has to make sure the items are indeed unique. --- openapi/index_openapi.json | 41 ++++++++--- openapi/openapi.json | 99 +++++++++++++++++++++------ optimade/models/entries.py | 2 +- optimade/models/jsonapi.py | 16 +++-- optimade/models/optimade_json.py | 28 ++------ optimade/models/responses.py | 54 +++++++++++---- optimade/server/exception_handlers.py | 25 ++++--- 7 files changed, 178 insertions(+), 87 deletions(-) diff --git a/openapi/index_openapi.json b/openapi/index_openapi.json index c2629d3fd..95a106e61 100644 --- a/openapi/index_openapi.json +++ b/openapi/index_openapi.json @@ -505,6 +505,7 @@ "properties": { "data": { "title": "Data", + "uniqueItems": true, "anyOf": [ { "allOf": [ @@ -517,14 +518,19 @@ "type": "array", "items": { "$ref": "#/components/schemas/Resource" - }, - "uniqueItems": true + } } ], "description": "Outputted Data" }, "meta": { - "$ref": "#/components/schemas/ResponseMeta" + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/ResponseMeta" + } + ], + "description": "A meta object containing non-standard information" }, "errors": { "title": "Errors", @@ -542,7 +548,7 @@ "items": { "$ref": "#/components/schemas/Resource" }, - "description": "A list of resources that are included" + "description": "A list of unique included resources" }, "links": { "title": "Links", @@ -765,7 +771,13 @@ "$ref": "#/components/schemas/IndexInfoResource" }, "meta": { - "$ref": "#/components/schemas/ResponseMeta" + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/ResponseMeta" + } + ], + "description": "A meta object containing non-standard information" }, "errors": { "title": "Errors", @@ -774,7 +786,7 @@ "items": { "$ref": "#/components/schemas/Error" }, - "description": "A list of errors" + "description": "A list of unique errors" }, "included": { "title": "Included", @@ -783,7 +795,7 @@ "items": { "$ref": "#/components/schemas/Resource" }, - "description": "A list of resources that are included" + "description": "A list of unique included resources" }, "links": { "title": "Links", @@ -1002,6 +1014,7 @@ "properties": { "data": { "title": "Data", + "uniqueItems": true, "anyOf": [ { "type": "array", @@ -1018,7 +1031,13 @@ ] }, "meta": { - "$ref": "#/components/schemas/ResponseMeta" + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/ResponseMeta" + } + ], + "description": "A meta object containing non-standard information" }, "errors": { "title": "Errors", @@ -1027,10 +1046,11 @@ "items": { "$ref": "#/components/schemas/Error" }, - "description": "A list of errors" + "description": "A list of unique errors" }, "included": { "title": "Included", + "uniqueItems": true, "anyOf": [ { "type": "array", @@ -1215,6 +1235,7 @@ }, "data": { "title": "Data", + "uniqueItems": true, "anyOf": [ { "allOf": [ @@ -1475,6 +1496,7 @@ }, "warnings": { "title": "Warnings", + "uniqueItems": true, "type": "array", "items": { "$ref": "#/components/schemas/Warnings" @@ -1514,6 +1536,7 @@ }, "data": { "title": "Data", + "uniqueItems": true, "anyOf": [ { "allOf": [ diff --git a/openapi/openapi.json b/openapi/openapi.json index 10fa3bbee..36ef6797e 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -1164,7 +1164,13 @@ "$ref": "#/components/schemas/EntryInfoResource" }, "meta": { - "$ref": "#/components/schemas/ResponseMeta" + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/ResponseMeta" + } + ], + "description": "A meta object containing non-standard information" }, "errors": { "title": "Errors", @@ -1173,7 +1179,7 @@ "items": { "$ref": "#/components/schemas/Error" }, - "description": "A list of errors" + "description": "A list of unique errors" }, "included": { "title": "Included", @@ -1182,7 +1188,7 @@ "items": { "$ref": "#/components/schemas/Resource" }, - "description": "A list of resources that are included" + "description": "A list of unique included resources" }, "links": { "title": "Links", @@ -1403,6 +1409,7 @@ "properties": { "data": { "title": "Data", + "uniqueItems": true, "anyOf": [ { "allOf": [ @@ -1415,14 +1422,19 @@ "type": "array", "items": { "$ref": "#/components/schemas/Resource" - }, - "uniqueItems": true + } } ], "description": "Outputted Data" }, "meta": { - "$ref": "#/components/schemas/ResponseMeta" + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/ResponseMeta" + } + ], + "description": "A meta object containing non-standard information" }, "errors": { "title": "Errors", @@ -1440,7 +1452,7 @@ "items": { "$ref": "#/components/schemas/Resource" }, - "description": "A list of resources that are included" + "description": "A list of unique included resources" }, "links": { "title": "Links", @@ -1554,7 +1566,13 @@ "$ref": "#/components/schemas/BaseInfoResource" }, "meta": { - "$ref": "#/components/schemas/ResponseMeta" + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/ResponseMeta" + } + ], + "description": "A meta object containing non-standard information" }, "errors": { "title": "Errors", @@ -1563,7 +1581,7 @@ "items": { "$ref": "#/components/schemas/Error" }, - "description": "A list of errors" + "description": "A list of unique errors" }, "included": { "title": "Included", @@ -1572,7 +1590,7 @@ "items": { "$ref": "#/components/schemas/Resource" }, - "description": "A list of resources that are included" + "description": "A list of unique included resources" }, "links": { "title": "Links", @@ -1772,6 +1790,7 @@ "properties": { "data": { "title": "Data", + "uniqueItems": true, "anyOf": [ { "type": "array", @@ -1788,7 +1807,13 @@ ] }, "meta": { - "$ref": "#/components/schemas/ResponseMeta" + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/ResponseMeta" + } + ], + "description": "A meta object containing non-standard information" }, "errors": { "title": "Errors", @@ -1797,10 +1822,11 @@ "items": { "$ref": "#/components/schemas/Error" }, - "description": "A list of errors" + "description": "A list of unique errors" }, "included": { "title": "Included", + "uniqueItems": true, "anyOf": [ { "type": "array", @@ -2008,6 +2034,7 @@ }, "data": { "title": "Data", + "uniqueItems": true, "anyOf": [ { "allOf": [ @@ -2257,6 +2284,7 @@ "properties": { "data": { "title": "Data", + "uniqueItems": true, "anyOf": [ { "type": "array", @@ -2273,7 +2301,13 @@ ] }, "meta": { - "$ref": "#/components/schemas/ResponseMeta" + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/ResponseMeta" + } + ], + "description": "A meta object containing non-standard information" }, "errors": { "title": "Errors", @@ -2282,10 +2316,11 @@ "items": { "$ref": "#/components/schemas/Error" }, - "description": "A list of errors" + "description": "A list of unique errors" }, "included": { "title": "Included", + "uniqueItems": true, "anyOf": [ { "type": "array", @@ -2341,7 +2376,13 @@ ] }, "meta": { - "$ref": "#/components/schemas/ResponseMeta" + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/ResponseMeta" + } + ], + "description": "A meta object containing non-standard information" }, "errors": { "title": "Errors", @@ -2350,10 +2391,11 @@ "items": { "$ref": "#/components/schemas/Error" }, - "description": "A list of errors" + "description": "A list of unique errors" }, "included": { "title": "Included", + "uniqueItems": true, "anyOf": [ { "type": "array", @@ -2602,6 +2644,7 @@ }, "warnings": { "title": "Warnings", + "uniqueItems": true, "type": "array", "items": { "$ref": "#/components/schemas/Warnings" @@ -2683,6 +2726,7 @@ }, "data": { "title": "Data", + "uniqueItems": true, "anyOf": [ { "allOf": [ @@ -2979,6 +3023,7 @@ "properties": { "data": { "title": "Data", + "uniqueItems": true, "anyOf": [ { "type": "array", @@ -2995,7 +3040,13 @@ ] }, "meta": { - "$ref": "#/components/schemas/ResponseMeta" + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/ResponseMeta" + } + ], + "description": "A meta object containing non-standard information" }, "errors": { "title": "Errors", @@ -3004,10 +3055,11 @@ "items": { "$ref": "#/components/schemas/Error" }, - "description": "A list of errors" + "description": "A list of unique errors" }, "included": { "title": "Included", + "uniqueItems": true, "anyOf": [ { "type": "array", @@ -3063,7 +3115,13 @@ ] }, "meta": { - "$ref": "#/components/schemas/ResponseMeta" + "title": "Meta", + "allOf": [ + { + "$ref": "#/components/schemas/ResponseMeta" + } + ], + "description": "A meta object containing non-standard information" }, "errors": { "title": "Errors", @@ -3072,10 +3130,11 @@ "items": { "$ref": "#/components/schemas/Error" }, - "description": "A list of errors" + "description": "A list of unique errors" }, "included": { "title": "Included", + "uniqueItems": true, "anyOf": [ { "type": "array", diff --git a/optimade/models/entries.py b/optimade/models/entries.py index 600a52017..5bdc10385 100644 --- a/optimade/models/entries.py +++ b/optimade/models/entries.py @@ -1,7 +1,7 @@ # pylint: disable=line-too-long,no-self-argument from datetime import datetime from typing import Optional, Dict, List -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field, validator # pylint: disable=no-name-in-module from .jsonapi import Relationships, Attributes, Resource from .optimade_json import Relationship diff --git a/optimade/models/jsonapi.py b/optimade/models/jsonapi.py index adf05ec0a..8119f5fb1 100644 --- a/optimade/models/jsonapi.py +++ b/optimade/models/jsonapi.py @@ -1,6 +1,6 @@ """This module should reproduce JSON API v1.0 https://jsonapi.org/format/1.0/""" # pylint: disable=no-self-argument -from typing import Optional, Union, List, Set +from typing import Optional, Union, List from pydantic import ( # pylint: disable=no-name-in-module BaseModel, AnyUrl, @@ -161,7 +161,7 @@ class Relationship(BaseModel): description="a links object containing at least one of the following: self, related", ) data: Optional[Union[BaseResource, List[BaseResource]]] = Field( - None, description="Resource linkage" + None, description="Resource linkage", uniqueItems=True ) meta: Optional[Meta] = Field( None, @@ -255,16 +255,18 @@ class Resource(BaseResource): class Response(BaseModel): """A top-level response""" - data: Optional[Union[None, Resource, Set[Resource]]] = Field( - None, description="Outputted Data" + data: Optional[Union[None, Resource, List[Resource]]] = Field( + None, description="Outputted Data", uniqueItems=True ) meta: Optional[Meta] = Field( None, description="A meta object containing non-standard information related to the Success", ) - errors: Optional[Set[Error]] = Field(None, description="A list of errors") - included: Optional[Set[Resource]] = Field( - None, description="A list of resources that are included" + errors: Optional[List[Error]] = Field( + None, description="A list of unique errors", uniqueItems=True + ) + included: Optional[List[Resource]] = Field( + None, description="A list of unique included resources", uniqueItems=True ) links: Optional[ToplevelLinks] = Field( None, description="Links associated with the primary data or errors" diff --git a/optimade/models/optimade_json.py b/optimade/models/optimade_json.py index 04999b61c..e2fdbdbc0 100644 --- a/optimade/models/optimade_json.py +++ b/optimade/models/optimade_json.py @@ -1,7 +1,7 @@ """Modified JSON API v1.0 for OPTiMaDe API""" # pylint: disable=no-self-argument,no-name-in-module from pydantic import Field, root_validator, BaseModel, AnyHttpUrl, EmailStr -from typing import Optional, Union, List, Set +from typing import Optional, Union, List from datetime import datetime @@ -15,7 +15,6 @@ "Implementation", "ResponseMeta", "OptimadeError", - "Failure", "Success", "Warnings", "BaseRelationshipMeta", @@ -187,34 +186,15 @@ class ResponseMeta(jsonapi.Meta): 'which MUST have the value "warning". The field detail MUST be present and SHOULD contain a non-critical message, ' "e.g., reporting unrecognized search attributes or deprecated features. The field status, representing a HTTP " "response status code, MUST NOT be present for a warning resource object. This is an exclusive field for error resource objects.", + uniqueItems=True, ) -class Failure(jsonapi.Response): - """errors MUST be present and data MUST be skipped""" - - meta: Optional[ResponseMeta] = Field( - None, - description="A meta object containing non-standard information related to the Success", - ) - errors: Set[OptimadeError] = Field( - ..., - description="A list of OPTiMaDe-specific JSON API error objects, where the field detail MUST be present.", - ) - - @root_validator(pre=True) - def data_must_be_skipped(cls, values): - if values.get("data", None) is not None: - raise ValueError("data MUST be skipped for failures reporting errors") - return values - - class Success(jsonapi.Response): """errors are not allowed""" meta: Optional[ResponseMeta] = Field( - None, - description="A meta object containing non-standard information related to the Success", + None, description="A meta object containing non-standard information" ) @root_validator(pre=True) @@ -255,4 +235,4 @@ class Relationship(jsonapi.Relationship): data: Optional[ Union[BaseRelationshipResource, List[BaseRelationshipResource]] - ] = Field(None, description="Resource linkage") + ] = Field(None, description="Resource linkage", uniqueItems=True) diff --git a/optimade/models/responses.py b/optimade/models/responses.py index 55f0ad863..5baf9386f 100644 --- a/optimade/models/responses.py +++ b/optimade/models/responses.py @@ -1,12 +1,14 @@ +# pylint: disable=no-self-argument from typing import Union, List, Optional, Dict, Any -from pydantic import Field +from pydantic import Field, root_validator +from .jsonapi import Response from .baseinfo import BaseInfoResource from .entries import EntryInfoResource, EntryResource from .index_metadb import IndexInfoResource from .links import LinksResource -from .optimade_json import Success, Failure, ResponseMeta +from .optimade_json import Success, ResponseMeta, OptimadeError from .references import ReferenceResource from .structures import StructureResource @@ -26,39 +28,57 @@ ) -class ErrorResponse(Failure): - meta: Optional[ResponseMeta] = Field(None) +class ErrorResponse(Response): + """errors MUST be present and data MUST be skipped""" + + meta: Optional[ResponseMeta] = Field( + None, description="A meta object containing non-standard information" + ) + errors: List[OptimadeError] = Field( + ..., + description="A list of OPTiMaDe-specific JSON API error objects, where the field detail MUST be present.", + uniqueItems=True, + ) + + @root_validator(pre=True) + def data_must_be_skipped(cls, values): + if values.get("data", None) is not None: + raise ValueError("data MUST be skipped for failures reporting errors") + return values class IndexInfoResponse(Success): - meta: Optional[ResponseMeta] = Field(None) data: IndexInfoResource = Field(...) class EntryInfoResponse(Success): - meta: Optional[ResponseMeta] = Field(None) data: EntryInfoResource = Field(...) class InfoResponse(Success): - meta: Optional[ResponseMeta] = Field(None) data: BaseInfoResource = Field(...) class EntryResponseOne(Success): - meta: Optional[ResponseMeta] = Field(None) data: Union[EntryResource, Dict[str, Any], None] = Field(...) - included: Optional[Union[List[EntryResource], List[Dict[str, Any]]]] = Field(None) + included: Optional[Union[List[EntryResource], List[Dict[str, Any]]]] = Field( + None, uniqueItems=True + ) class EntryResponseMany(Success): - meta: Optional[ResponseMeta] = Field(None) - data: Union[List[EntryResource], List[Dict[str, Any]]] = Field(...) - included: Optional[Union[List[EntryResource], List[Dict[str, Any]]]] = Field(None) + data: Union[List[EntryResource], List[Dict[str, Any]]] = Field( + ..., uniqueItems=True + ) + included: Optional[Union[List[EntryResource], List[Dict[str, Any]]]] = Field( + None, uniqueItems=True + ) class LinksResponse(EntryResponseMany): - data: Union[List[LinksResource], List[Dict[str, Any]]] = Field(...) + data: Union[List[LinksResource], List[Dict[str, Any]]] = Field( + ..., uniqueItems=True + ) class StructureResponseOne(EntryResponseOne): @@ -66,7 +86,9 @@ class StructureResponseOne(EntryResponseOne): class StructureResponseMany(EntryResponseMany): - data: Union[List[StructureResource], List[Dict[str, Any]]] = Field(...) + data: Union[List[StructureResource], List[Dict[str, Any]]] = Field( + ..., uniqueItems=True + ) class ReferenceResponseOne(EntryResponseOne): @@ -74,4 +96,6 @@ class ReferenceResponseOne(EntryResponseOne): class ReferenceResponseMany(EntryResponseMany): - data: Union[List[ReferenceResource], List[Dict[str, Any]]] = Field(...) + data: Union[List[ReferenceResource], List[Dict[str, Any]]] = Field( + ..., uniqueItems=True + ) diff --git a/optimade/server/exception_handlers.py b/optimade/server/exception_handlers.py index 2d3a36976..32cba547d 100644 --- a/optimade/server/exception_handlers.py +++ b/optimade/server/exception_handlers.py @@ -1,5 +1,5 @@ import traceback -from typing import Dict, Any +from typing import List from lark.exceptions import VisitError @@ -17,7 +17,10 @@ def general_exception( - request: Request, exc: Exception, **kwargs: Dict[str, Any] + request: Request, + exc: Exception, + status_code: int = 500, # A status_code in `exc` will take precedence + errors: List[OptimadeError] = None, ) -> JSONResponse: tb = "".join( traceback.format_exception(etype=type(exc), value=exc, tb=exc.__traceback__) @@ -25,9 +28,9 @@ def general_exception( print(tb) try: - status_code = exc.status_code + http_response_code = exc.status_code except AttributeError: - status_code = kwargs.get("status_code", 500) + http_response_code = status_code try: title = exc.title @@ -36,9 +39,8 @@ def general_exception( detail = getattr(exc, "detail", str(exc)) - errors = kwargs.get("errors", None) - if not errors: - errors = [OptimadeError(detail=detail, status=status_code, title=title)] + if errors is None: + errors = [OptimadeError(detail=detail, status=http_response_code, title=title)] try: response = ErrorResponse( @@ -60,7 +62,8 @@ def general_exception( response = ErrorResponse(errors=errors) return JSONResponse( - status_code=status_code, content=jsonable_encoder(response, exclude_unset=True) + status_code=http_response_code, + content=jsonable_encoder(response, exclude_unset=True), ) @@ -75,18 +78,18 @@ def request_validation_exception_handler(request: Request, exc: RequestValidatio def validation_exception_handler(request: Request, exc: ValidationError): status = 500 title = "ValidationError" - errors = [] + errors = set() for error in exc.errors(): pointer = "/" + "/".join([str(_) for _ in error["loc"]]) source = ErrorSource(pointer=pointer) code = error["type"] detail = error["msg"] - errors.append( + errors.add( OptimadeError( detail=detail, status=status, title=title, source=source, code=code ) ) - return general_exception(request, exc, status_code=status, errors=errors) + return general_exception(request, exc, status_code=status, errors=list(errors)) def grammar_not_implemented_handler(request: Request, exc: VisitError): From f8d190fa0bf1875debfd35a1a7d4a0f04803c846 Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen Date: Fri, 28 Feb 2020 16:18:57 +0100 Subject: [PATCH 6/6] Update descriptions for response models --- openapi/index_openapi.json | 11 +++++++-- openapi/openapi.json | 43 ++++++++++++++++++++++++++++-------- optimade/models/responses.py | 28 ++++++++++++++++------- 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/openapi/index_openapi.json b/openapi/index_openapi.json index 95a106e61..c8da03391 100644 --- a/openapi/index_openapi.json +++ b/openapi/index_openapi.json @@ -768,7 +768,13 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/IndexInfoResource" + "title": "Data", + "allOf": [ + { + "$ref": "#/components/schemas/IndexInfoResource" + } + ], + "description": "Index meta-database /info data" }, "meta": { "title": "Meta", @@ -1028,7 +1034,8 @@ "type": "object" } } - ] + ], + "description": "List of unique OPTIMADE links resource objects" }, "meta": { "title": "Meta", diff --git a/openapi/openapi.json b/openapi/openapi.json index 36ef6797e..54c9d564e 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -1161,7 +1161,13 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/EntryInfoResource" + "title": "Data", + "allOf": [ + { + "$ref": "#/components/schemas/EntryInfoResource" + } + ], + "description": "OPTIMADE information for an entry endpoint" }, "meta": { "title": "Meta", @@ -1563,7 +1569,13 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/BaseInfoResource" + "title": "Data", + "allOf": [ + { + "$ref": "#/components/schemas/BaseInfoResource" + } + ], + "description": "The implementations /info data" }, "meta": { "title": "Meta", @@ -1804,7 +1816,8 @@ "type": "object" } } - ] + ], + "description": "List of unique OPTIMADE links resource objects" }, "meta": { "title": "Meta", @@ -2298,7 +2311,8 @@ "type": "object" } } - ] + ], + "description": "List of unique OPTIMADE references entry resource objects" }, "meta": { "title": "Meta", @@ -2368,12 +2382,17 @@ "title": "Data", "anyOf": [ { - "$ref": "#/components/schemas/ReferenceResource" + "allOf": [ + { + "$ref": "#/components/schemas/ReferenceResource" + } + ] }, { "type": "object" } - ] + ], + "description": "A single references entry resource" }, "meta": { "title": "Meta", @@ -3037,7 +3056,8 @@ "type": "object" } } - ] + ], + "description": "List of unique OPTIMADE structures entry resource objects" }, "meta": { "title": "Meta", @@ -3107,12 +3127,17 @@ "title": "Data", "anyOf": [ { - "$ref": "#/components/schemas/StructureResource" + "allOf": [ + { + "$ref": "#/components/schemas/StructureResource" + } + ] }, { "type": "object" } - ] + ], + "description": "A single structures entry resource" }, "meta": { "title": "Meta", diff --git a/optimade/models/responses.py b/optimade/models/responses.py index 5baf9386f..de48f1c02 100644 --- a/optimade/models/responses.py +++ b/optimade/models/responses.py @@ -48,15 +48,17 @@ def data_must_be_skipped(cls, values): class IndexInfoResponse(Success): - data: IndexInfoResource = Field(...) + data: IndexInfoResource = Field(..., description="Index meta-database /info data") class EntryInfoResponse(Success): - data: EntryInfoResource = Field(...) + data: EntryInfoResource = Field( + ..., description="OPTIMADE information for an entry endpoint" + ) class InfoResponse(Success): - data: BaseInfoResource = Field(...) + data: BaseInfoResource = Field(..., description="The implementations /info data") class EntryResponseOne(Success): @@ -77,25 +79,35 @@ class EntryResponseMany(Success): class LinksResponse(EntryResponseMany): data: Union[List[LinksResource], List[Dict[str, Any]]] = Field( - ..., uniqueItems=True + ..., + description="List of unique OPTIMADE links resource objects", + uniqueItems=True, ) class StructureResponseOne(EntryResponseOne): - data: Union[StructureResource, Dict[str, Any], None] = Field(...) + data: Union[StructureResource, Dict[str, Any], None] = Field( + ..., description="A single structures entry resource" + ) class StructureResponseMany(EntryResponseMany): data: Union[List[StructureResource], List[Dict[str, Any]]] = Field( - ..., uniqueItems=True + ..., + description="List of unique OPTIMADE structures entry resource objects", + uniqueItems=True, ) class ReferenceResponseOne(EntryResponseOne): - data: Union[ReferenceResource, Dict[str, Any], None] = Field(...) + data: Union[ReferenceResource, Dict[str, Any], None] = Field( + ..., description="A single references entry resource" + ) class ReferenceResponseMany(EntryResponseMany): data: Union[List[ReferenceResource], List[Dict[str, Any]]] = Field( - ..., uniqueItems=True + ..., + description="List of unique OPTIMADE references entry resource objects", + uniqueItems=True, )