Skip to content

Commit e9ea867

Browse files
authored
Merge pull request #1324 from Sage-Bionetworks/SYNPY-1768
[SYNPY-1768] Conditional dependencies are now grouped
2 parents 73700bd + 5913dbc commit e9ea867

6 files changed

Lines changed: 192 additions & 207 deletions

File tree

synapseclient/extensions/curator/schema_generation.py

Lines changed: 105 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5053,7 +5053,7 @@ def _update_nodes_to_process(self, nodes: list[str]) -> None:
50535053

50545054

50555055
@dataclass
5056-
class JSONSchema: # pylint: disable=too-many-instance-attributes
5056+
class JSONSchema:
50575057
"""
50585058
A dataclass representing a JSON Schema.
50595059
Each attribute represents a keyword in a JSON Schema.
@@ -5066,7 +5066,9 @@ class JSONSchema: # pylint: disable=too-many-instance-attributes
50665066
description: An optional description of the object described by this schema.
50675067
properties: A list of property schemas.
50685068
required: A list of properties required by the schema.
5069-
all_of: A list of conditions the schema must meet. This should be removed if empty.
5069+
_conditional_dependencies: A mapping of conditional dependencies to be added to the "allOf" keyword in JSON Schema.
5070+
The key is a tuple of (watched_property, enum_value)
5071+
The value is a list of properties that become required when watched_property has the value enum_value.
50705072
"""
50715073

50725074
schema_id: str = ""
@@ -5076,7 +5078,9 @@ class JSONSchema: # pylint: disable=too-many-instance-attributes
50765078
description: str = "TBD"
50775079
properties: dict[str, Property] = field(default_factory=dict)
50785080
required: list[str] = field(default_factory=list)
5079-
all_of: list[AllOf] = field(default_factory=list)
5081+
_conditional_dependencies: dict[tuple[str, str], list[str]] = field(
5082+
default_factory=dict
5083+
)
50805084

50815085
def as_json_schema_dict(
50825086
self,
@@ -5088,15 +5092,25 @@ def as_json_schema_dict(
50885092
The dataclass as a dict.
50895093
"""
50905094
json_schema_dict = asdict(self)
5095+
# Change dataclass attribute names to JSON Schema keywords
5096+
# Dataclass attributes can't start with $
50915097
keywords_to_change = {
50925098
"schema_id": "$id",
50935099
"schema": "$schema",
5094-
"all_of": "allOf",
50955100
}
50965101
for old_word, new_word in keywords_to_change.items():
50975102
json_schema_dict[new_word] = json_schema_dict.pop(old_word)
5098-
if not self.all_of:
5099-
json_schema_dict.pop("allOf")
5103+
5104+
# Converts the conditional dependencies to allOf conditions and adds them to the JSON Schema dict
5105+
# The conditional dependencies are not added to the JSON Schema dict directly because they are not
5106+
# in the correct format to be added as is, and they need to be converted to allOf conditions first.
5107+
# Finally the conditional dependencies are removed from the JSON Schema dict because they are not a
5108+
# valid JSON Schema keyword
5109+
if self._conditional_dependencies:
5110+
json_schema_dict["allOf"] = self._convert_conditional_properties_to_all_of(
5111+
self._conditional_dependencies
5112+
)
5113+
json_schema_dict.pop("_conditional_dependencies")
51005114
return json_schema_dict
51015115

51025116
def add_required_property(self, name: str) -> None:
@@ -5108,15 +5122,6 @@ def add_required_property(self, name: str) -> None:
51085122
"""
51095123
self.required.append(name)
51105124

5111-
def add_to_all_of_list(self, item: AllOf) -> None:
5112-
"""
5113-
Adds a property to the all_of list
5114-
5115-
Arguments:
5116-
item: The item to add to the all_of list
5117-
"""
5118-
self.all_of.append(item)
5119-
51205125
def update_property(self, property_dict: dict[str, Property]) -> None:
51215126
"""
51225127
Updates the property dict
@@ -5142,47 +5147,97 @@ def update_property(self, property_dict: dict[str, Property]) -> None:
51425147
)
51435148
self.properties.update(property_dict)
51445149

5150+
def add_conditional_dependency(
5151+
self, watched_property: str, enum_value: str, dependent_property: str
5152+
) -> None:
5153+
"""
5154+
Adds a conditional dependency to the conditional dependencies dict
5155+
Arguments:
5156+
watched_property: The property that is being watched
5157+
enum_value: The value of the watched property that triggers the condition
5158+
dependent_property: The property that becomes required when the condition is triggered
5159+
"""
5160+
if (watched_property, enum_value) not in self._conditional_dependencies:
5161+
self._conditional_dependencies[(watched_property, enum_value)] = [
5162+
dependent_property
5163+
]
5164+
else:
5165+
self._conditional_dependencies[(watched_property, enum_value)].append(
5166+
dependent_property
5167+
)
5168+
5169+
@staticmethod
5170+
def _convert_conditional_properties_to_all_of(
5171+
conditional_dependencies: dict[tuple[str, str], list[str]]
5172+
) -> list[AllOf]:
5173+
"""
5174+
Converts the conditional dependencies dict to a list of JSON Schema allOf conditions
5175+
5176+
Arguments:
5177+
conditional_dependencies: A mapping of conditional dependencies to be added to the "allOf" keyword in JSON Schema.
5178+
The key is a tuple of (watched_property, enum_value)
5179+
The value is a list of properties that become required when watched_property has the value enum_value.
5180+
5181+
Returns:
5182+
A list of JSON Schema allOf conditions
5183+
5184+
5185+
For example:
5186+
In the example data model the Patient component has the Diagnosis attribute.
5187+
The Diagnosis attribute has valid values of ["Healthy", "Cancer"].
5188+
The Cancer valid value is also an attribute that dependsOn on the
5189+
attributes Cancer Type and Family History
5190+
Cancer Type and Family History are attributes with valid values.
5191+
Therefore: When Diagnosis == "Cancer", Cancer Type and Family History should become required
5192+
5193+
Example conditional schema:
5194+
"if":{
5195+
"properties":{
5196+
"Diagnosis":{
5197+
"enum":[
5198+
"Cancer"
5199+
]
5200+
}
5201+
}
5202+
},
5203+
"then":{
5204+
"properties":{
5205+
"FamilyHistory":{
5206+
"not":{
5207+
"type":"null"
5208+
}
5209+
}
5210+
},
5211+
"required":["FamilyHistory"]
5212+
}
5213+
"""
5214+
all_of = []
5215+
for (
5216+
watched_property,
5217+
enum_value,
5218+
), dependent_properties in conditional_dependencies.items():
5219+
conditional_dep = {
5220+
"if": {"properties": {watched_property: {"enum": [enum_value]}}},
5221+
"then": {
5222+
"properties": {
5223+
prop: {"not": {"type": "null"}} for prop in dependent_properties
5224+
},
5225+
"required": dependent_properties,
5226+
},
5227+
}
5228+
all_of.append(conditional_dep)
5229+
return all_of
5230+
51455231

51465232
def _set_conditional_dependencies(
51475233
json_schema: JSONSchema,
51485234
graph_state: GraphTraversalState,
51495235
use_display_labels: bool = True,
51505236
) -> None:
51515237
"""
5152-
This sets conditional requirements in the "allOf" keyword.
5238+
This translates conditional requirements in a JSONSchema object from a GraphTraversalState object
51535239
This is used when certain properties are required depending on the value of another property.
51545240
5155-
For example:
5156-
In the example data model the Patient component has the Diagnosis attribute.
5157-
The Diagnosis attribute has valid values of ["Healthy", "Cancer"].
5158-
The Cancer valid value is also an attribute that dependsOn on the
5159-
attributes Cancer Type and Family History
5160-
Cancer Type and Family History are attributes with valid values.
5161-
Therefore: When Diagnosis == "Cancer", Cancer Type and Family History should become required
5162-
5163-
Example conditional schema:
5164-
"if":{
5165-
"properties":{
5166-
"Diagnosis":{
5167-
"enum":[
5168-
"Cancer"
5169-
]
5170-
}
5171-
}
5172-
},
5173-
"then":{
5174-
"properties":{
5175-
"FamilyHistory":{
5176-
"not":{
5177-
"type":"null"
5178-
}
5179-
}
5180-
},
5181-
"required":[
5182-
"FamilyHistory"
5183-
]
5184-
}
5185-
51865241
Arguments:
51875242
json_schema: The JSON Scheme where the node might be set as a property
51885243
graph_state: The instance tracking the current state of the graph
@@ -5201,14 +5256,9 @@ def _set_conditional_dependencies(
52015256
conditional_properties = graph_state.get_conditional_properties(use_display_labels)
52025257
for prop in conditional_properties:
52035258
attribute, value = prop
5204-
conditional_schema = {
5205-
"if": {"properties": {attribute: {"enum": [value]}}},
5206-
"then": {
5207-
"properties": {node_name: {"not": {"type": "null"}}},
5208-
"required": [node_name],
5209-
},
5210-
}
5211-
json_schema.add_to_all_of_list(conditional_schema)
5259+
json_schema.add_conditional_dependency(
5260+
watched_property=attribute, enum_value=value, dependent_property=node_name
5261+
)
52125262

52135263

52145264
def _create_enum_array_property(

tests/unit/synapseclient/extensions/schema_files/expected_jsonschemas/expected.BulkRNA-seqAssay.display_names_schema.json

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,33 +41,16 @@
4141
"not": {
4242
"type": "null"
4343
}
44-
}
45-
},
46-
"required": [
47-
"Genome Build"
48-
]
49-
}
50-
},
51-
{
52-
"if": {
53-
"properties": {
54-
"File Format": {
55-
"enum": [
56-
"CSV/TSV"
57-
]
58-
}
59-
}
60-
},
61-
"then": {
62-
"properties": {
63-
"Genome Build": {
44+
},
45+
"Genome FASTA": {
6446
"not": {
6547
"type": "null"
6648
}
6749
}
6850
},
6951
"required": [
70-
"Genome Build"
52+
"Genome Build",
53+
"Genome FASTA"
7154
]
7255
}
7356
},
@@ -76,21 +59,21 @@
7659
"properties": {
7760
"File Format": {
7861
"enum": [
79-
"CRAM"
62+
"CSV/TSV"
8063
]
8164
}
8265
}
8366
},
8467
"then": {
8568
"properties": {
86-
"Genome FASTA": {
69+
"Genome Build": {
8770
"not": {
8871
"type": "null"
8972
}
9073
}
9174
},
9275
"required": [
93-
"Genome FASTA"
76+
"Genome Build"
9477
]
9578
}
9679
}

tests/unit/synapseclient/extensions/schema_files/expected_jsonschemas/expected.BulkRNA-seqAssay.schema.json

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,33 +41,16 @@
4141
"not": {
4242
"type": "null"
4343
}
44-
}
45-
},
46-
"required": [
47-
"GenomeBuild"
48-
]
49-
}
50-
},
51-
{
52-
"if": {
53-
"properties": {
54-
"FileFormat": {
55-
"enum": [
56-
"CSV/TSV"
57-
]
58-
}
59-
}
60-
},
61-
"then": {
62-
"properties": {
63-
"GenomeBuild": {
44+
},
45+
"GenomeFASTA": {
6446
"not": {
6547
"type": "null"
6648
}
6749
}
6850
},
6951
"required": [
70-
"GenomeBuild"
52+
"GenomeBuild",
53+
"GenomeFASTA"
7154
]
7255
}
7356
},
@@ -76,21 +59,21 @@
7659
"properties": {
7760
"FileFormat": {
7861
"enum": [
79-
"CRAM"
62+
"CSV/TSV"
8063
]
8164
}
8265
}
8366
},
8467
"then": {
8568
"properties": {
86-
"GenomeFASTA": {
69+
"GenomeBuild": {
8770
"not": {
8871
"type": "null"
8972
}
9073
}
9174
},
9275
"required": [
93-
"GenomeFASTA"
76+
"GenomeBuild"
9477
]
9578
}
9679
}

tests/unit/synapseclient/extensions/schema_files/expected_jsonschemas/expected.Patient.display_names_schema.json

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,15 @@
1818
"not": {
1919
"type": "null"
2020
}
21-
}
22-
},
23-
"required": [
24-
"Cancer Type"
25-
]
26-
}
27-
},
28-
{
29-
"if": {
30-
"properties": {
31-
"Diagnosis": {
32-
"enum": [
33-
"Cancer"
34-
]
35-
}
36-
}
37-
},
38-
"then": {
39-
"properties": {
21+
},
4022
"Family History": {
4123
"not": {
4224
"type": "null"
4325
}
4426
}
4527
},
4628
"required": [
29+
"Cancer Type",
4730
"Family History"
4831
]
4932
}

0 commit comments

Comments
 (0)