Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add relationships functionality #91

Merged
merged 28 commits into from
Nov 27, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0db31e3
Added relationships to two documents in test data
ml-evs Nov 21, 2019
114fee0
Subclassed Relationships model as EntryRelationships
ml-evs Nov 21, 2019
595503a
Use lists of resources rather than sets and validate uniqueness manually
ml-evs Nov 21, 2019
fd97a85
Added included key to entry responses
ml-evs Nov 21, 2019
86a5156
Collect included data from query, without doing full request yet
ml-evs Nov 21, 2019
60256c4
Rework structuremapper to allow relationships through
ml-evs Nov 21, 2019
b00fceb
Return included as field in response
ml-evs Nov 21, 2019
58565d4
Added simple tests for relationships & included
ml-evs Nov 21, 2019
28441a2
Updated validator patches
ml-evs Nov 21, 2019
4d9379b
Merge branch 'master' into close_#71_add_relationships
CasperWA Nov 22, 2019
13d1e00
Merge remote-tracking branch 'origin/master' into close_#71_add_relat…
CasperWA Nov 22, 2019
15e5266
Tidy included set determination
ml-evs Nov 25, 2019
fc5068b
AssertionErrors->ValueError to avoid strange pydantic behaviour
ml-evs Nov 25, 2019
cdc1400
Validate references based on type
ml-evs Nov 25, 2019
e698476
Fixed dummy reference ID to only lowercase
ml-evs Nov 26, 2019
a80f6b8
Perform extra queries to populate included field in response
ml-evs Nov 26, 2019
0c85a71
Switch compound query to OR...
ml-evs Nov 26, 2019
bf9e169
Use EntryListingQueryParams instead of dict
ml-evs Nov 27, 2019
67871b7
Update and fix tests and test data
ml-evs Nov 27, 2019
a89e120
del -> pop
ml-evs Nov 27, 2019
0ae9834
del -> pop again
ml-evs Nov 27, 2019
543eca9
Make entry collections importable from main
ml-evs Nov 27, 2019
b6f9ea5
Remove any optional query params arguments
ml-evs Nov 27, 2019
5c6569d
AssertionError -> ValueError for validators
CasperWA Nov 27, 2019
e197ea7
Merge branch 'master' into close_#71_add_relationships
CasperWA Nov 27, 2019
ba488c8
Update type of included in entry responses
ml-evs Nov 27, 2019
2e47e83
Added description inside reference test data
ml-evs Nov 27, 2019
b9290a5
Added simple validation tests for references
ml-evs Nov 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 26 additions & 4 deletions optimade/models/entries.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# pylint: disable=line-too-long
from datetime import datetime
from typing import Optional, Dict, List
from pydantic import BaseModel, Schema
from typing import Optional, Dict, List, Union
from pydantic import BaseModel, Schema, validator

from .jsonapi import Relationships, Attributes, Resource, Relationship

Expand All @@ -15,11 +15,33 @@
)


class TypedRelationship(Relationship):
ml-evs marked this conversation as resolved.
Show resolved Hide resolved
@validator("data", whole=True)
def check_rel_type(cls, data, values):
if hasattr(cls, "_req_type") and any(obj.type != cls._req_type for obj in data):
raise ValueError("Object stored in relationship data has wrong type")
return data


class ReferenceRelationship(TypedRelationship):
_req_type = "references"


class StructureRelationship(TypedRelationship):
_req_type = "structures"


class EntryRelationships(Relationships):
"""This model wraps the JSON API Relationships to include type-specific top level keys. """

references: Optional[Relationship] = Schema(
..., description="Object containing links to relationships with other entries."
references: Optional[ReferenceRelationship] = Schema(
...,
description="Object containing links to relationships with entries of the `references` type.",
)

structures: Optional[StructureRelationship] = Schema(
...,
description="Object containing links to relationships with entries of the `structures` type.",
)


Expand Down
8 changes: 4 additions & 4 deletions optimade/models/jsonapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class RelationshipLinks(BaseModel):
@validator("related", always=True)
def either_self_or_related_must_be_specified(cls, v, values):
if values.get("self", None) is None and v is None:
raise AssertionError(
raise ValueError(
"Either 'self' or 'related' MUST be specified for RelationshipLinks"
)
return v
Expand Down Expand Up @@ -167,7 +167,7 @@ def at_least_one_relationship_key_must_be_set(cls, v, values):
and values.get("data", None) is None
and v is None
):
raise AssertionError(
raise ValueError(
"Either 'links', 'data', or 'meta' MUST be specified for relationship"
)
return v
Expand All @@ -186,7 +186,7 @@ class Relationships(BaseModel):

@validator("id", "type")
def check_illegal_relationships_fields(cls, v):
raise AssertionError('"id", "type" MUST NOT be fields under relationships')
raise ValueError('"id", "type" MUST NOT be fields under relationships')


class ResourceLinks(BaseModel):
Expand Down Expand Up @@ -271,7 +271,7 @@ def either_data_meta_or_errors_must_be_set(cls, v, values):
and values.get("meta", None) is None
and v is None
):
raise AssertionError(
raise ValueError(
"Either 'data', 'meta', or 'errors' must be specified in the top-level response"
)
return v
6 changes: 6 additions & 0 deletions optimade/models/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ def test_simple_relationships(self):
}
EntryRelationships(**relationship)

relationship = {
"references": {"data": [{"id": "Dijkstra1968", "type": "structures"}]}
}
with self.assertRaises(ValidationError):
EntryRelationships(**relationship)


def test_constrained_list():
class ConListModel(BaseModel):
Expand Down
19 changes: 9 additions & 10 deletions optimade/server/entry_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,19 +119,18 @@ def find(
fields = set(params.response_fields.split(","))
else:
fields = all_fields.copy()
results = []

# build up set of included data with unique IDs
included = []
id_set = set()
results = []
included = {}
for doc in self.collection.find(**criteria):
results.append(self.resource_cls(**self.resource_mapper.map_back(doc)))
for reference in (
doc.get("relationships", {}).get("references", {}).get("data", [])
):
if reference["id"] not in id_set:
included.append(reference)
id_set.add(reference["id"])
# collapse list of references into dict by ID and take only one per ID
refs = doc.get("relationships", {}).get("references", {}).get("data", [])
CasperWA marked this conversation as resolved.
Show resolved Hide resolved
for ref in refs:
# could check here and raise a warning if any IDs clash
CasperWA marked this conversation as resolved.
Show resolved Hide resolved
included[ref["id"]] = ref

included = list(included.values())

if isinstance(params, SingleEntryQueryParams):
results = results[0] if results else None
Expand Down