Skip to content

Commit 123d00d

Browse files
schema: add source workflows
1 parent 1433411 commit 123d00d

File tree

6 files changed

+342
-14
lines changed

6 files changed

+342
-14
lines changed

cylc/uiserver/schema.py

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,37 @@
2525
import graphene
2626
from graphene.types.generic import GenericScalar
2727

28+
from cylc.flow.data_store_mgr import (
29+
DELTA_ADDED,
30+
)
2831
from cylc.flow.id import Tokens
2932
from cylc.flow.network.schema import (
3033
CyclePoint,
3134
GenericResponse,
3235
ID,
33-
SortArgs,
34-
Task,
3536
Mutations,
3637
Queries,
37-
process_resolver_info,
3838
STRIP_NULL_DEFAULT,
39+
SortArgs,
3940
Subscriptions,
41+
Task,
42+
Workflow,
4043
WorkflowID,
4144
_mut_field,
45+
get_nodes_all,
46+
get_workflows,
47+
process_resolver_info,
4248
sstrip,
43-
get_nodes_all
4449
)
4550
from cylc.uiserver.resolvers import (
4651
Resolvers,
4752
list_log_files,
4853
stream_log,
4954
)
55+
from cylc.uiserver.services.source_workflows import (
56+
list_source_workflows,
57+
get_workflow_source,
58+
)
5059

5160
if TYPE_CHECKING:
5261
from graphql import ResolveInfo
@@ -484,17 +493,37 @@ class UISTask(Task):
484493
count = graphene.Int()
485494

486495

487-
class UISQueries(Queries):
496+
class SourceWorkflow(graphene.ObjectType):
497+
"""A Cylc workflow source directory.
498+
499+
This may or may not be located within the configured cylc source
500+
directories. For workflows located outside of the configured
501+
directories, the "name" field will allways be null.
502+
"""
503+
name = graphene.String(
504+
description='The name of the source workflow'
505+
)
506+
path = graphene.String(
507+
description='The location of the source workflow.'
508+
)
488509

489-
class LogFiles(graphene.ObjectType):
490-
# Example GraphiQL query:
491-
# {
492-
# logFiles(workflowID: "<workflow_id>", task: "<task_id>") {
493-
# files
494-
# }
495-
# }
496-
files = graphene.List(graphene.String)
497510

511+
class UISWorkflow(Workflow):
512+
source = graphene.Field(
513+
SourceWorkflow,
514+
resolver=get_workflow_source,
515+
)
516+
517+
518+
class LogFiles(graphene.ObjectType):
519+
files = graphene.List(graphene.String)
520+
521+
522+
class UISQueries(Queries):
523+
source_workflows = graphene.List(
524+
SourceWorkflow,
525+
resolver=list_source_workflows,
526+
)
498527
log_files = graphene.Field(
499528
LogFiles,
500529
description='List available job logs',
@@ -505,7 +534,6 @@ class LogFiles(graphene.ObjectType):
505534
),
506535
resolver=list_log_files
507536
)
508-
509537
tasks = graphene.List(
510538
UISTask,
511539
description=Task._meta.description,
@@ -521,6 +549,20 @@ class LogFiles(graphene.ObjectType):
521549
sort=SortArgs(default_value=None),
522550
)
523551

552+
workflows = graphene.List(
553+
UISWorkflow,
554+
description=Workflow._meta.description,
555+
ids=graphene.List(ID, default_value=[]),
556+
exids=graphene.List(ID, default_value=[]),
557+
# TODO: Change these defaults post #3500 in coordination with WUI
558+
strip_null=graphene.Boolean(default_value=False),
559+
delta_store=graphene.Boolean(default_value=False),
560+
delta_type=graphene.String(default_value=DELTA_ADDED),
561+
initial_burst=graphene.Boolean(default_value=True),
562+
ignore_interval=graphene.Float(default_value=2.5),
563+
resolver=get_workflows
564+
)
565+
524566

525567
class UISSubscriptions(Subscriptions):
526568
# Example graphiql workflow log subscription:

cylc/uiserver/services/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
2+
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
3+
#
4+
# This program is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
2+
#
3+
# This program is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
"""Utilities relating to the listing and management of source workflows."""
17+
18+
from contextlib import suppress
19+
from pathlib import Path
20+
from typing import Optional, List, Dict
21+
22+
from cylc.flow.cfgspec.glbl_cfg import glbl_cfg
23+
from cylc.flow.id import Tokens
24+
from cylc.flow.network.scan import scan_multi
25+
from cylc.flow.pathutil import get_workflow_run_dir
26+
from cylc.flow.workflow_files import get_workflow_source_dir
27+
28+
29+
# the user's configured workflow source directories
30+
SOURCE_DIRS: List[Path] = [
31+
Path(source_dir).expanduser()
32+
for source_dir in glbl_cfg().get(['install', 'source dirs'])
33+
]
34+
35+
36+
SourceWorkflow = Dict
37+
38+
39+
def _source_workflow(source_path: Path) -> SourceWorkflow:
40+
"""Return the fields required to resolve a SourceWorkflow.
41+
42+
Args:
43+
source_path:
44+
Path to the source workflow directory.
45+
46+
"""
47+
return {
48+
'name': _get_source_workflow_name(source_path),
49+
'path': source_path,
50+
}
51+
52+
53+
def _blank_source_workflow() -> SourceWorkflow:
54+
"""Return a blank source workflow.
55+
56+
This will be used for workflows which were not installed by "cylc install".
57+
"""
58+
59+
return {'name': None, 'path': None}
60+
61+
62+
def _get_source_workflow_name(source_path: Path) -> Optional[str]:
63+
"""Return the "name" of the source workflow.
64+
65+
This is the "name" that can be provided to the "cylc install" command.
66+
67+
Args:
68+
source_path:
69+
Path to the source workflow directory.
70+
71+
Returns:
72+
The source workflow name if the source workflow is located within
73+
a configured source directory, else None.
74+
75+
"""
76+
for source_dir in SOURCE_DIRS:
77+
with suppress(ValueError):
78+
return str(source_path.relative_to(source_dir))
79+
return None
80+
81+
82+
def _get_workflow_source(workflow_id):
83+
"""Return the source workflow for the given workflow ID."""
84+
run_dir = get_workflow_run_dir(workflow_id)
85+
source_dir, _symlink = get_workflow_source_dir(run_dir)
86+
if source_dir:
87+
return _source_workflow(Path(source_dir))
88+
return _blank_source_workflow()
89+
90+
91+
async def list_source_workflows(*_) -> List[SourceWorkflow]:
92+
"""List source workflows located in the configured source directories."""
93+
ret = []
94+
async for flow in scan_multi(SOURCE_DIRS):
95+
ret.append(_source_workflow(flow['path']))
96+
return ret
97+
98+
99+
def get_workflow_source(data, _, **kwargs) -> Optional[SourceWorkflow]:
100+
"""Resolve the source for an installed workflow.
101+
102+
If the source cannot be resolved, e.g. if the workflow was not installed by
103+
"cylc install", then this will return None.
104+
"""
105+
workflow_id = Tokens(data.id)['workflow']
106+
return _get_workflow_source(workflow_id)

cylc/uiserver/tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import zmq
3030

3131
from jupyter_server.auth.identity import User
32+
from _pytest.monkeypatch import MonkeyPatch
3233

3334
from cylc.flow.cfgspec.glbl_cfg import glbl_cfg
3435
from cylc.flow.id import Tokens
@@ -411,3 +412,10 @@ def _inner(cached=False):
411412

412413
yield _mock_glbl_cfg
413414
rmtree(tmp_path)
415+
416+
417+
@pytest.fixture(scope='module')
418+
def mod_monkeypatch():
419+
monkeypatch = MonkeyPatch()
420+
yield monkeypatch
421+
monkeypatch.undo()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
2+
#
3+
# This program is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program. If not, see <http://www.gnu.org/licenses/>.

0 commit comments

Comments
 (0)