Skip to content

Commit 211ae11

Browse files
author
Jennings Zhang
committed
caw export
1 parent a0e1d98 commit 211ae11

File tree

10 files changed

+200
-8
lines changed

10 files changed

+200
-8
lines changed

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,24 @@ wow
194194
└── e.txt
195195
```
196196

197+
#### `caw export`
198+
199+
Export a registered pipeline to JSON.
200+
201+
```shell
202+
caw export 'Automatic Fetal Brain Reconstruction Pipeline v1.0.0' > pipeline.json
203+
204+
curl -u "chris:chris1234" "https://example.com/api/v1/pipelines/" \
205+
-H 'Content-Type:application/vnd.collection+json' \
206+
-H 'Accept:application/vnd.collection+json' \
207+
--data "$(< pipeline.json)"
208+
```
209+
210+
##### `caw export` Limitations
211+
212+
- All plugin parameters will be exported as part of `plugin_parameter_defaults`
213+
- Order of lists, such as `plugin_tree` and `plugin_parameter_defaults`, may not be the same as the original
214+
197215
## Development
198216

199217
```shell
@@ -237,8 +255,6 @@ docker run --rm --net=host --userns=host -v $PWD:/usr/local/src/caw:ro \
237255

238256
## Roadmap
239257

240-
Bugs will be fixed, but new features will not be added.
241-
242258
For the next-generation _ChRIS_ client, see
243259
[chrs](https://github.com/FNNDSC/chrs),
244260
and how it compares to `caw`:

caw/commands/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
import caw.commands.download
99
import caw.commands.pipeline
1010
import caw.commands.search
11+
import caw.commands.export

caw/commands/export.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import typer
2+
3+
from caw.commands.store import app, build_client
4+
5+
6+
@app.command()
7+
def export(name: str = typer.Argument(..., help='Name of pipeline.')):
8+
"""
9+
Deserialize a pipeline to JSON.
10+
"""
11+
client = build_client()
12+
pipeline = client.get_pipeline(name)
13+
typer.echo(pipeline.deserialize())

chris/cube/pipeline.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import abc
23
from dataclasses import dataclass
34
from chris.cube.plugin_tree import PluginTree
@@ -9,7 +10,45 @@ class Pipeline(abc.ABC):
910
authors: str
1011
description: str
1112
category: str
13+
locked: bool
1214

1315
@abc.abstractmethod
1416
def get_root(self) -> PluginTree:
1517
...
18+
19+
def deserialize(self) -> str:
20+
"""
21+
Produce a JSON representation which can be uploaded to a CUBE instance.
22+
"""
23+
data = [
24+
{
25+
'name': 'name',
26+
'value': self.name
27+
},
28+
{
29+
'name': 'authors',
30+
'value': self.authors,
31+
},
32+
{
33+
'name': 'category',
34+
'value': self.category,
35+
},
36+
{
37+
'name': 'description',
38+
'value': self.description,
39+
},
40+
{
41+
'name': 'locked',
42+
'value': self.locked,
43+
},
44+
{
45+
'name': 'plugin_tree',
46+
'value': self.get_root().deserialize()
47+
}
48+
]
49+
template = {
50+
'template': {
51+
'data': data
52+
}
53+
}
54+
return json.dumps(template)

chris/cube/plugin_tree.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import json
2+
from functools import cached_property
13
from dataclasses import dataclass, field
2-
from typing import Generator, Collection, Dict, Tuple
4+
from typing import Generator, Collection, Dict, Tuple, Optional
35
from collections import deque
4-
from chris.types import ParameterType, PluginInstanceId
6+
from chris.types import ParameterType, PluginInstanceId, PipingId
57
from chris.cube.plugin import Plugin
68
from chris.cube.plugin_instance import PluginInstance
79
from chris.cube.resource import ConnectedResource
@@ -79,10 +81,56 @@ def __iter__(self):
7981
return self.dfs()
8082

8183
def __len__(self):
84+
return self.length
85+
86+
@cached_property
87+
def length(self) -> int:
8288
count = 0
8389
for _ in self:
8490
count += 1
8591
return count
8692

8793
def __contains__(self, __x: object) -> bool:
8894
return any(__x == e for e in self)
95+
96+
def deserialize(self) -> str:
97+
"""
98+
Produce this Plugin Tree as JSON according to the ``plugin_tree`` schema
99+
of the *ChRIS* pipeline spec.
100+
"""
101+
return json.dumps(self.deserialize_tree())
102+
103+
def deserialize_tree(self) -> list:
104+
data = []
105+
index_map: dict[Optional[PipingId], Optional[int]] = {
106+
None: None
107+
}
108+
109+
for node in self.bfs():
110+
index_map[node.piping.id] = len(data)
111+
previous_index = index_map[node.piping.previous_id]
112+
data.append(node.deserialize_node(previous_index))
113+
114+
return data
115+
116+
def deserialize_node(self, previous_index: Optional[int]) -> dict:
117+
"""
118+
Deserialize just this ``PluginTree`` (and not its children).
119+
"""
120+
plugin = self.get_plugin()
121+
data = {
122+
'plugin_name': plugin.name,
123+
'plugin_version': plugin.version,
124+
'previous_index': previous_index
125+
}
126+
127+
if self.default_parameters:
128+
data['plugin_parameter_defaults'] = [
129+
{
130+
'name': name,
131+
'default': value
132+
}
133+
for name, value in self.default_parameters.items()
134+
]
135+
136+
return data

chris/cube/registered_pipeline.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ def freeze(self) -> PluginTree:
6060
@dataclass(frozen=True)
6161
class RegisteredPipeline(CUBEResource, Pipeline):
6262
id: PipelineId
63-
locked: bool
6463
owner_username: CUBEUsername
6564
creation_date: ISOFormatDateString
6665
modification_date: ISOFormatDateString

chris/tests/test_deserialize.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import json
2+
from chris.client import ChrisClient
3+
4+
expected = r"""
5+
{"template":
6+
{"data":[{"name":"name","value":"Example branching pipeline"},
7+
{"name": "authors", "value": "Jennings Zhang <[email protected]>"},
8+
{"name": "category", "value": "Example"},
9+
{"name": "description", "value":
10+
"A more complicated but nonetheless useless pipeline."},
11+
{"name":"locked","value":false},
12+
{"name":"plugin_tree","value":"[
13+
{\"plugin_name\":\"pl-simpledsapp\",\"plugin_version\":\"2.0.2\",\"previous_index\":null,
14+
\"plugin_parameter_defaults\":[{\"name\":\"prefix\",\"default\":\"a\"}]},
15+
{\"plugin_name\":\"pl-simpledsapp\",\"plugin_version\":\"2.0.2\",\"previous_index\":0,
16+
\"plugin_parameter_defaults\":[{\"name\":\"prefix\",\"default\":\"b\"}]},
17+
{\"plugin_name\":\"pl-simpledsapp\",\"plugin_version\":\"2.0.2\",\"previous_index\":0,
18+
\"plugin_parameter_defaults\":[{\"name\":\"prefix\",\"default\":\"c\"}]},
19+
{\"plugin_name\":\"pl-simpledsapp\",\"plugin_version\":\"2.0.2\",\"previous_index\":1,
20+
\"plugin_parameter_defaults\":[{\"name\":\"prefix\",\"default\":\"d\"}]},
21+
{\"plugin_name\":\"pl-simpledsapp\",\"plugin_version\":\"2.0.2\",\"previous_index\":2,
22+
\"plugin_parameter_defaults\":[{\"name\":\"prefix\",\"default\":\"e\"}]},
23+
{\"plugin_name\":\"pl-simpledsapp\",\"plugin_version\":\"2.0.2\",\"previous_index\":2,
24+
\"plugin_parameter_defaults\":[{\"name\":\"prefix\",\"default\":\"f\"}]},
25+
{\"plugin_name\":\"pl-simpledsapp\",\"plugin_version\":\"2.0.2\",\"previous_index\":2,
26+
\"plugin_parameter_defaults\":[{\"name\":\"prefix\",\"default\":\"g\"}]},
27+
{\"plugin_name\":\"pl-simpledsapp\",\"plugin_version\":\"2.0.2\",\"previous_index\":6,
28+
\"plugin_parameter_defaults\":[{\"name\":\"prefix\",\"default\":\"h\"}]}
29+
]"}]}}
30+
"""
31+
expected = expected.replace('\n', ' ')
32+
33+
34+
def test_deserialize():
35+
client = ChrisClient.from_login(address='http://localhost:8000/api/v1/',
36+
username='chris', password='chris1234')
37+
pipeline = client.get_pipeline('Example branching pipeline')
38+
expected_plugin_tree = json.loads(json.loads(expected)['template']['data'][-1]['value'])
39+
actual = pipeline.get_root().deserialize_tree()
40+
41+
# limitation: retrieved plugin tree will have all parameter defaults
42+
for piping in actual:
43+
__remove_defaults_except_prefix(piping)
44+
45+
assert sorted_pipings(actual) == sorted_pipings(expected_plugin_tree)
46+
47+
48+
def __remove_defaults_except_prefix(piping: dict) -> None:
49+
piping['plugin_parameter_defaults'] = [
50+
p for p in piping['plugin_parameter_defaults']
51+
if p['name'] == 'prefix'
52+
]
53+
54+
55+
def sorted_pipings(plugin_tree: list) -> list:
56+
return sorted(plugin_tree, key=SerializedPiping)
57+
58+
59+
class SerializedPiping:
60+
def __init__(self, __d: dict):
61+
self.plugin_name = __d['plugin_name']
62+
self.plugin_version = __d['plugin_version']
63+
self.previous_index = __d['previous_index']
64+
65+
def __lt__(self, other: 'SerializedPiping') -> bool:
66+
if self.previous_index is None:
67+
return True
68+
if other.previous_index is None:
69+
return False
70+
if self.previous_index < other.previous_index:
71+
return True
72+
if self.plugin_name < other.plugin_name:
73+
return True
74+
if self.plugin_version < other.plugin_version:
75+
return True
76+
return False

examples/dummy_pipeline.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ cat << EOF
4747
{"template":
4848
{"data":[{"name":"name","value":"Example branching pipeline"},
4949
{"name": "authors", "value": "Jennings Zhang <[email protected]>"},
50-
{"name": "Category", "value": "Example"},
50+
{"name": "category", "value": "Example"},
5151
{"name": "description", "value":
5252
"A more complicated but nonetheless useless pipeline."},
5353
{"name":"locked","value":false},

examples/upload_reconstruction_pipeline.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ cat << EOF
3939
{"template":
4040
{"data":[{"name":"name","value":"Automatic Fetal Brain Reconstruction Pipeline v1.0.0"},
4141
{"name": "authors", "value": "Jennings Zhang <[email protected]>"},
42-
{"name": "Category", "value": "MRI"},
42+
{"name": "category", "value": "MRI"},
4343
{"name": "description", "value":
4444
"Automatic fetal brain reconstruction pipeline developed by Kiho's group at the FNNDSC. Features machine-learning based brain masking and quality assessment."},
4545
{"name":"locked","value":false},

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
setup(
88
name='caw',
9-
version='0.5.0',
9+
version='0.6.0',
1010
packages=find_packages(exclude=('*.tests',)),
1111
url='https://github.com/FNNDSC/caw',
1212
license='MIT',

0 commit comments

Comments
 (0)