Repository: Materials-Consortia/optimade-python-tools
Pull request #131 (2020-01-13 11:05:08)

Add meta.description to BaseRelationshipResource
Sub-classed BaseResource to introduce special OPTiMaDe requirement of
having an OPTIONAL `meta` field in the relationship resource, which
MUST have the field `description`, it it's present.

Also, introduced some pylint disables for the models to avoid pylint
complaining about long lines and wrong use of `self` for validators
(which are class methods, hence they should NOT have the `self`
argument).


Patch:
diff --git a/openapi/index_openapi.json b/openapi/index_openapi.json
index a53f6a6f..3bb04fff 100644
--- a/openapi/index_openapi.json
+++ b/openapi/index_openapi.json
@@ -606,8 +606,23 @@
         },
         "description": "A JSON object containing information about an available API version"
       },
-      "BaseResource": {
-        "title": "BaseResource",
+      "BaseRealationshipMeta": {
+        "title": "BaseRealationshipMeta",
+        "required": [
+          "description"
+        ],
+        "type": "object",
+        "properties": {
+          "description": {
+            "title": "Description",
+            "type": "string",
+            "description": "OPTIONAL human-readable description of the relationship"
+          }
+        },
+        "description": "Specific meta field for base relationship resource"
+      },
+      "BaseRelationshipResource": {
+        "title": "BaseRelationshipResource",
         "required": [
           "id",
           "type"
@@ -623,9 +638,18 @@
             "title": "Type",
             "type": "string",
             "description": "Resource type"
+          },
+          "meta": {
+            "title": "Meta",
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/BaseRealationshipMeta"
+              }
+            ],
+            "description": "Relationship meta field. MUST contain 'description' if supplied."
           }
         },
-        "description": "Minimum requirements to represent a Resource"
+        "description": "Minimum requirements to represent a relationship resource"
       },
       "EntryRelationships": {
         "title": "EntryRelationships",
@@ -1417,14 +1441,14 @@
               {
                 "allOf": [
                   {
-                    "$ref": "#/components/schemas/BaseResource"
+                    "$ref": "#/components/schemas/BaseRelationshipResource"
                   }
                 ]
               },
               {
                 "type": "array",
                 "items": {
-                  "$ref": "#/components/schemas/BaseResource"
+                  "$ref": "#/components/schemas/BaseRelationshipResource"
                 }
               }
             ],
@@ -1440,7 +1464,7 @@
             "description": "a meta object that contains non-standard meta-information about the relationship."
           }
         },
-        "description": "Representation references from the resource object in which it\u2019s defined to other resource objects."
+        "description": "Similar to normal JSON API relationship, but with addition of OPTIONAL meta field for a resource"
       },
       "RelatedChildResource": {
         "title": "RelatedChildResource",
@@ -1716,14 +1740,14 @@
               {
                 "allOf": [
                   {
-                    "$ref": "#/components/schemas/BaseResource"
+                    "$ref": "#/components/schemas/BaseRelationshipResource"
                   }
                 ]
               },
               {
                 "type": "array",
                 "items": {
-                  "$ref": "#/components/schemas/BaseResource"
+                  "$ref": "#/components/schemas/BaseRelationshipResource"
                 }
               }
             ],
@@ -1739,7 +1763,7 @@
             "description": "a meta object that contains non-standard meta-information about the relationship."
           }
         },
-        "description": "Representation references from the resource object in which it\u2019s defined to other resource objects."
+        "description": "Similar to normal JSON API relationship, but with addition of OPTIONAL meta field for a resource"
       },
       "ToplevelLinks": {
         "title": "ToplevelLinks",
diff --git a/openapi/openapi.json b/openapi/openapi.json
index 74477055..5d39877f 100644
--- a/openapi/openapi.json
+++ b/openapi/openapi.json
@@ -2342,8 +2342,23 @@
         },
         "description": "Resource objects appear in a JSON:API document to represent resources."
       },
-      "BaseResource": {
-        "title": "BaseResource",
+      "BaseRealationshipMeta": {
+        "title": "BaseRealationshipMeta",
+        "required": [
+          "description"
+        ],
+        "type": "object",
+        "properties": {
+          "description": {
+            "title": "Description",
+            "type": "string",
+            "description": "OPTIONAL human-readable description of the relationship"
+          }
+        },
+        "description": "Specific meta field for base relationship resource"
+      },
+      "BaseRelationshipResource": {
+        "title": "BaseRelationshipResource",
         "required": [
           "id",
           "type"
@@ -2359,9 +2374,18 @@
             "title": "Type",
             "type": "string",
             "description": "Resource type"
+          },
+          "meta": {
+            "title": "Meta",
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/BaseRealationshipMeta"
+              }
+            ],
+            "description": "Relationship meta field. MUST contain 'description' if supplied."
           }
         },
-        "description": "Minimum requirements to represent a Resource"
+        "description": "Minimum requirements to represent a relationship resource"
       },
       "EntryInfoProperty": {
         "title": "EntryInfoProperty",
@@ -3168,14 +3192,14 @@
               {
                 "allOf": [
                   {
-                    "$ref": "#/components/schemas/BaseResource"
+                    "$ref": "#/components/schemas/BaseRelationshipResource"
                   }
                 ]
               },
               {
                 "type": "array",
                 "items": {
-                  "$ref": "#/components/schemas/BaseResource"
+                  "$ref": "#/components/schemas/BaseRelationshipResource"
                 }
               }
             ],
@@ -3191,7 +3215,7 @@
             "description": "a meta object that contains non-standard meta-information about the relationship."
           }
         },
-        "description": "Representation references from the resource object in which it\u2019s defined to other resource objects."
+        "description": "Similar to normal JSON API relationship, but with addition of OPTIONAL meta field for a resource"
       },
       "ReferenceResource": {
         "title": "ReferenceResource",
@@ -3845,14 +3869,14 @@
               {
                 "allOf": [
                   {
-                    "$ref": "#/components/schemas/BaseResource"
+                    "$ref": "#/components/schemas/BaseRelationshipResource"
                   }
                 ]
               },
               {
                 "type": "array",
                 "items": {
-                  "$ref": "#/components/schemas/BaseResource"
+                  "$ref": "#/components/schemas/BaseRelationshipResource"
                 }
               }
             ],
@@ -3868,7 +3892,7 @@
             "description": "a meta object that contains non-standard meta-information about the relationship."
           }
         },
-        "description": "Representation references from the resource object in which it\u2019s defined to other resource objects."
+        "description": "Similar to normal JSON API relationship, but with addition of OPTIONAL meta field for a resource"
       },
       "StructureResource": {
         "title": "StructureResource",
diff --git a/optimade/models/baseinfo.py b/optimade/models/baseinfo.py
index 32df32dc..3109b63f 100644
--- a/optimade/models/baseinfo.py
+++ b/optimade/models/baseinfo.py
@@ -1,3 +1,4 @@
+# pylint: disable=no-self-argument
 import re
 
 from typing import Dict, List, Optional
diff --git a/optimade/models/entries.py b/optimade/models/entries.py
index 8c854de7..b78bc66d 100644
--- a/optimade/models/entries.py
+++ b/optimade/models/entries.py
@@ -1,9 +1,10 @@
-# pylint: disable=line-too-long
+# 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 .jsonapi import Relationships, Attributes, Resource, Relationship
+from .jsonapi import Relationships, Attributes, Resource
+from .optimade_json import Relationship
 
 
 __all__ = (
diff --git a/optimade/models/jsonapi.py b/optimade/models/jsonapi.py
index 5767a65a..05ab1a72 100644
--- a/optimade/models/jsonapi.py
+++ b/optimade/models/jsonapi.py
@@ -1,6 +1,7 @@
 """This module should reproduce JSON API v1.0 https://jsonapi.org/format/1.0/"""
-from typing import Optional, Set, Union, Any, List
-from pydantic import BaseModel, AnyUrl, Field, validator, root_validator
+# pylint: disable=no-self-argument
+from typing import Optional, Set, Union, List
+from pydantic import BaseModel, AnyUrl, Field, root_validator
 
 
 __all__ = (
diff --git a/optimade/models/links.py b/optimade/models/links.py
index d6409468..0e1c34c2 100644
--- a/optimade/models/links.py
+++ b/optimade/models/links.py
@@ -1,3 +1,4 @@
+# pylint: disable=no-self-argument
 from pydantic import Field, AnyUrl, validator, root_validator
 from typing import Union
 
diff --git a/optimade/models/optimade_json.py b/optimade/models/optimade_json.py
index a3263ca7..55c0dda5 100644
--- a/optimade/models/optimade_json.py
+++ b/optimade/models/optimade_json.py
@@ -1,11 +1,20 @@
 """Modified JSON API v1.0 for OPTiMaDe API"""
-from pydantic import Field, validator, root_validator
-from typing import Optional, Set
+# pylint: disable=no-self-argument
+from pydantic import Field, root_validator, BaseModel
+from typing import Optional, Set, Union, List
 
 from . import jsonapi
 
 
-__all__ = ("Error", "Failure", "Success", "Warnings")
+__all__ = (
+    "Error",
+    "Failure",
+    "Success",
+    "Warnings",
+    "BaseRealationshipMeta",
+    "BaseRelationshipResource",
+    "Relationship",
+)
 
 
 class Error(jsonapi.Error):
@@ -91,3 +100,28 @@ class Warnings(Error):
         if values.get("status", None) is not None:
             raise ValueError("status MUST NOT be specified for warnings")
         return values
+
+
+class BaseRealationshipMeta(BaseModel):
+    """Specific meta field for base relationship resource"""
+
+    description: str = Field(
+        ..., description="OPTIONAL human-readable description of the relationship"
+    )
+
+
+class BaseRelationshipResource(jsonapi.BaseResource):
+    """Minimum requirements to represent a relationship resource"""
+
+    meta: Optional[BaseRealationshipMeta] = Field(
+        None,
+        description="Relationship meta field. MUST contain 'description' if supplied.",
+    )
+
+
+class Relationship(jsonapi.Relationship):
+    """Similar to normal JSON API relationship, but with addition of OPTIONAL meta field for a resource"""
+
+    data: Optional[
+        Union[BaseRelationshipResource, List[BaseRelationshipResource]]
+    ] = Field(None, description="Resource linkage")
diff --git a/optimade/models/references.py b/optimade/models/references.py
index 27e8acd2..9198cce6 100644
--- a/optimade/models/references.py
+++ b/optimade/models/references.py
@@ -1,3 +1,4 @@
+# pylint: disable=line-too-long,no-self-argument
 from pydantic import Field, BaseModel, AnyUrl, validator
 from typing import List, Optional
 
diff --git a/optimade/models/toplevel.py b/optimade/models/toplevel.py
index 10ef0603..9160d4c8 100644
--- a/optimade/models/toplevel.py
+++ b/optimade/models/toplevel.py
@@ -1,14 +1,7 @@
 from datetime import datetime
 from typing import Union, List, Optional, Dict, Any
 
-from pydantic import (  # pylint: disable=no-name-in-module
-    BaseModel,
-    validator,
-    AnyUrl,
-    AnyHttpUrl,
-    Field,
-    EmailStr,
-)
+from pydantic import BaseModel, AnyHttpUrl, Field, EmailStr
 
 from .jsonapi import Link, Meta
 from .utils import NonnegativeInt


Test patch:
diff --git a/tests/models/test_models.py b/tests/models/test_models.py
index 072141d3..3c4e7ac0 100644
--- a/tests/models/test_models.py
+++ b/tests/models/test_models.py
@@ -59,18 +59,45 @@ class TestPydanticValidation(unittest.TestCase):
                 StructureResource(**StructureMapper.map_back(structure))
 
     def test_simple_relationships(self):
+        """Make sure relationship resources are added to the correct relationship"""
+
+        good_relationships = (
+            {"references": {"data": [{"id": "dijkstra1968", "type": "references"}]}},
+            {"structures": {"data": [{"id": "dijkstra1968", "type": "structures"}]}},
+        )
+        for relationship in good_relationships:
+            EntryRelationships(**relationship)
+
+        bad_relationships = (
+            {"references": {"data": [{"id": "dijkstra1968", "type": "structures"}]}},
+            {"structures": {"data": [{"id": "dijkstra1968", "type": "references"}]}},
+        )
+        for relationship in bad_relationships:
+            with self.assertRaises(ValidationError):
+                EntryRelationships(**relationship)
+
+    def test_advanced_relationships(self):
+        """Make sure the rules for the base resource 'meta' field are upheld"""
+
         relationship = {
-            "references": {"data": [{"id": "dijkstra1968", "type": "references"}]}
+            "references": {
+                "data": [
+                    {
+                        "id": "dijkstra1968",
+                        "type": "references",
+                        "meta": {
+                            "description": "Reference for the search algorithm Dijkstra."
+                        },
+                    }
+                ]
+            }
         }
         EntryRelationships(**relationship)
 
         relationship = {
-            "references": {"data": [{"id": "dijkstra1968", "type": "structures"}]}
-        }
-        with self.assertRaises(ValidationError):
-            EntryRelationships(**relationship)
-        relationship = {
-            "references": {"data": [{"id": "dijkstra1968", "type": "structures"}]}
+            "references": {
+                "data": [{"id": "dijkstra1968", "type": "references", "meta": {}}]
+            }
         }
         with self.assertRaises(ValidationError):
             EntryRelationships(**relationship)