Skip to content
This repository was archived by the owner on Aug 1, 2025. It is now read-only.

Commit e652b25

Browse files
authored
MRG: Merge pull request #118 from octue/allow-optional-strands
Allow optional strands
2 parents a7d7e8d + eb3c74f commit e652b25

File tree

10 files changed

+228
-421
lines changed

10 files changed

+228
-421
lines changed

.github/workflows/codeql.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535

3636
steps:
3737
- name: Checkout repository
38-
uses: actions/checkout@v3
38+
uses: actions/checkout@v4
3939

4040
# Initializes the CodeQL tools for scanning.
4141
- name: Initialize CodeQL

.github/workflows/python-ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ jobs:
2020
if: "!contains(github.event.head_commit.message, 'skipci')"
2121
runs-on: ubuntu-latest
2222
env:
23-
USING_COVERAGE: '3.9'
23+
USING_COVERAGE: '3.11'
2424
strategy:
2525
matrix:
26-
python: ['3.8', '3.9']
26+
python: ['3.8', '3.9', '3.10', '3.11']
2727
steps:
2828
- name: Checkout Repository
29-
uses: actions/checkout@v3
29+
uses: actions/checkout@v4
3030

3131
- name: Setup Python
3232
uses: actions/setup-python@v5
@@ -60,7 +60,7 @@ jobs:
6060
contents: read
6161
steps:
6262
- name: Checkout Repository
63-
uses: actions/checkout@v3
63+
uses: actions/checkout@v4
6464

6565
- name: Install Poetry
6666
uses: snok/[email protected]

.github/workflows/release.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ jobs:
1212
if: "github.event.pull_request.merged == true"
1313
runs-on: ubuntu-latest
1414
env:
15-
USING_COVERAGE: '3.9'
15+
USING_COVERAGE: '3.11'
1616
strategy:
1717
matrix:
18-
python: ['3.8', '3.9']
18+
python: ['3.8', '3.9', '3.10', '3.11']
1919
steps:
2020
- name: Checkout Repository
21-
uses: actions/checkout@v3
21+
uses: actions/checkout@v4
2222

2323
- name: Setup Python
2424
uses: actions/setup-python@v5
@@ -48,7 +48,7 @@ jobs:
4848
needs: run-tests
4949
runs-on: ubuntu-latest
5050
steps:
51-
- uses: actions/checkout@v3
51+
- uses: actions/checkout@v4
5252

5353
- name: Install Poetry
5454
uses: snok/[email protected]

poetry.lock

Lines changed: 109 additions & 392 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "twined"
3-
version = "0.5.6"
3+
version = "0.6.0"
44
repository = "https://www.github.com/octue/twined"
55
description = "A library to help digital twins and data services talk to one another."
66
authors = [
@@ -27,8 +27,6 @@ jsonschema = "^4"
2727
python-dotenv = ">=0,<=2"
2828

2929
[tool.poetry.group.dev.dependencies]
30-
flake8 = "3.8.3"
31-
black = "19.10.b0"
3230
pre-commit = ">=2.6.0"
3331
coverage = ">=5.2.1"
3432
numpy = "^1"

tests/test_manifest_strands.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,8 @@ def test_error_raised_if_multiple_datasets_have_same_name(self):
308308

309309
with self.assertRaises(KeyError):
310310
twine.validate_input_manifest(source=input_manifest)
311+
312+
def test_missing_optional_manifest_does_not_raise_error(self):
313+
"""Test that not providing an optional strand doesn't result in a validation error."""
314+
twine = Twine(source={"output_manifest": {"datasets": {}, "optional": True}})
315+
twine.validate(output_manifest=None)

tests/test_schema_strands.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ def test_valid_with_extra_values(self):
111111

112112
Twine(source=VALID_SCHEMA_TWINE).validate_configuration_values(source=configuration_valid_with_extra_field)
113113

114+
def test_missing_optional_values_do_not_raise_error(self):
115+
"""Test that not providing an optional strand doesn't result in a validation error."""
116+
twine = Twine(source={"configuration_values_schema": {"optional": True}})
117+
twine.validate(configuration_values=None)
118+
114119

115120
if __name__ == "__main__":
116121
unittest.main()

tests/test_twine.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,37 @@ def test_available_strands_properties(self):
124124
)
125125

126126
self.assertEqual(twine.available_manifest_strands, {"output_manifest"})
127+
128+
def test_required_strands_property(self):
129+
"""Test that the required strands property is correct."""
130+
twines = [
131+
{
132+
"configuration_values_schema": {},
133+
"input_values_schema": {},
134+
"output_values_schema": {},
135+
"output_manifest": {"datasets": {}},
136+
},
137+
{
138+
"configuration_values_schema": {"optional": True},
139+
"input_values_schema": {},
140+
"output_values_schema": {},
141+
"output_manifest": {"datasets": {}, "optional": True},
142+
},
143+
{
144+
"configuration_values_schema": {"optional": False},
145+
"input_values_schema": {},
146+
"output_values_schema": {},
147+
"output_manifest": {"datasets": {}, "optional": False},
148+
},
149+
]
150+
151+
expected_required_strands = [
152+
{"configuration_values", "input_values", "output_values", "output_manifest"},
153+
{"input_values", "output_values"},
154+
{"configuration_values", "input_values", "output_values", "output_manifest"},
155+
]
156+
157+
for twine, expected in zip(twines, expected_required_strands):
158+
with self.subTest(twine=twine):
159+
twine = Twine(source=twine)
160+
self.assertEqual(twine.required_strands, expected)

twined/schema/twine_schema.json

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
"manifest": {
3838
"type": "object",
3939
"properties": {
40+
"optional": {
41+
"type": "boolean",
42+
"description": "This should be `true` if the manifest is optional."
43+
},
4044
"datasets": {
4145
"type": "object",
4246
"description": "A list of entries, each describing a dataset that should be attached to / made available to the digital twin",
@@ -92,10 +96,20 @@
9296
]
9397
}
9498
},
95-
"configuration_manifest": {"$ref": "#/$defs/manifest"},
99+
"configuration_manifest": {
100+
"$ref": "#/$defs/manifest"
101+
},
96102
"configuration_values_schema": {
97103
"type": "object",
98-
"required": ["properties"]
104+
"properties": {
105+
"properties": {
106+
"type": "object"
107+
},
108+
"optional": {
109+
"type": "boolean",
110+
"description": "This should be `true` if the configuration values are optional."
111+
}
112+
}
99113
},
100114
"credentials": {
101115
"type": "array",
@@ -118,13 +132,35 @@
118132
"additionalProperties": false
119133
}
120134
},
121-
"input_manifest": {"$ref": "#/$defs/manifest"},
135+
"input_manifest": {
136+
"$ref": "#/$defs/manifest"
137+
},
122138
"input_values_schema": {
123-
"type": "object"
139+
"type": "object",
140+
"properties": {
141+
"properties": {
142+
"type": "object"
143+
},
144+
"optional": {
145+
"type": "boolean",
146+
"description": "This should be `true` if the input values are optional."
147+
}
148+
}
149+
},
150+
"output_manifest": {
151+
"$ref": "#/$defs/manifest"
124152
},
125-
"output_manifest": {"$ref": "#/$defs/manifest"},
126153
"output_values_schema": {
127-
"type": "object"
154+
"type": "object",
155+
"properties": {
156+
"properties": {
157+
"type": "object"
158+
},
159+
"optional": {
160+
"type": "boolean",
161+
"description": "This should be `true` if the output values are optional."
162+
}
163+
}
128164
}
129165
}
130166
}

twined/twine.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,16 @@ class Twine:
5555
"""
5656

5757
def __init__(self, **kwargs):
58+
self._available_strands = set()
59+
self._required_strands = set()
60+
5861
for name, strand in self._load_twine(**kwargs).items():
5962
setattr(self, name, strand)
63+
self._available_strands.add(trim_suffix(name, "_schema"))
64+
65+
if isinstance(strand, dict) and not strand.get("optional", False):
66+
self._required_strands.add(trim_suffix(name, "_schema"))
6067

61-
self._available_strands = set(trim_suffix(name, "_schema") for name in vars(self))
6268
self._available_manifest_strands = self._available_strands & set(MANIFEST_STRANDS)
6369

6470
def _load_twine(self, source=None):
@@ -216,18 +222,26 @@ def _validate_all_expected_datasets_are_present_in_manifest(self, manifest_kind,
216222
def available_strands(self):
217223
"""Get the names of strands that are found in this twine.
218224
219-
:return set:
225+
:return set(str):
220226
"""
221227
return self._available_strands
222228

223229
@property
224230
def available_manifest_strands(self):
225231
"""Get the names of the manifest strands that are found in this twine.
226232
227-
:return set:
233+
:return set(str):
228234
"""
229235
return self._available_manifest_strands
230236

237+
@property
238+
def required_strands(self):
239+
"""Get the names of strands that are required in this twine.
240+
241+
:return set(str):
242+
"""
243+
return self._required_strands
244+
231245
def validate_children(self, source, **kwargs):
232246
"""Validate that the children values, passed as either a file or a json string, are correct."""
233247
# TODO cache this loaded data keyed on a hashed version of kwargs
@@ -339,7 +353,7 @@ def _get_cls(name, cls):
339353
"""Getter that will return cls[name] if cls is a dict or cls otherwise"""
340354
return cls.get(name, None) if isinstance(cls, dict) else cls
341355

342-
def validate(self, allow_missing=False, allow_extra=False, cls=None, **kwargs):
356+
def validate(self, allow_extra=False, cls=None, **kwargs):
343357
"""Validate strands from sources provided as keyword arguments
344358
345359
Usage:
@@ -350,31 +364,29 @@ def validate(self, allow_missing=False, allow_extra=False, cls=None, **kwargs):
350364
credentials=credentials,
351365
children=children,
352366
cls=CLASS_MAP,
353-
allow_missing=False,
354367
allow_extra=False,
355368
)
356369
```
357370
358-
:param bool allow_missing: If strand is present in the twine, but the source is equal to None, allow validation to continue.
359371
:param bool allow_extra: If strand is present in the sources, but not in the twine, allow validation to continue (only strands in the twine will be validated and converted, others will be returned as-is)
360372
:param any cls: optional dict of classes keyed on strand name (alternatively, one single class which will be applied to strands) which will be instantiated with the validated source data.
361373
:return dict: dict of validated and initialised sources
362374
"""
363375
# pop any strand name:data pairs out of kwargs and into their own dict
364376
source_kwargs = tuple(name for name in kwargs.keys() if name in ALL_STRANDS)
365377
sources = dict((name, kwargs.pop(name)) for name in source_kwargs)
378+
366379
for strand_name, strand_data in sources.items():
367380
if not allow_extra:
368381
if (strand_data is not None) and (strand_name not in self.available_strands):
369382
raise exceptions.StrandNotFound(
370383
f"Source data is provided for '{strand_name}' but no such strand is defined in the twine"
371384
)
372385

373-
if not allow_missing:
374-
if (strand_name in self.available_strands) and (strand_data is None):
375-
raise exceptions.TwineValueException(
376-
f"The '{strand_name}' strand is defined in the twine, but no data is provided in sources"
377-
)
386+
if (strand_name in self.required_strands) and (strand_data is None):
387+
raise exceptions.TwineValueException(
388+
f"The '{strand_name}' strand is defined in the twine, but no data is provided in sources"
389+
)
378390

379391
if strand_data is not None:
380392
# TODO Consider reintroducing a skip based on whether cls is already instantiated. For now, leave it the

0 commit comments

Comments
 (0)