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

schema: add source workflows #512

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 64 additions & 15 deletions cylc/uiserver/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,43 @@
import graphene
from graphene.types.generic import GenericScalar

from cylc.flow.data_store_mgr import (
DELTA_ADDED,
)
from cylc.flow.id import Tokens
from cylc.flow.data_store_mgr import JOBS, TASKS
from cylc.flow.rundb import CylcWorkflowDAO
from cylc.flow.pathutil import get_workflow_run_dir
from cylc.flow.workflow_files import WorkflowFiles
from cylc.flow.network.schema import (
NODE_MAP,
CyclePoint,
GenericResponse,
SortArgs,
Task,
ID,
Job,
Mutations,
NODE_MAP,
Queries,
process_resolver_info,
STRIP_NULL_DEFAULT,
SortArgs,
Subscriptions,
Task,
Workflow,
WorkflowID,
_mut_field,
get_nodes_all,
get_workflows,
process_resolver_info,
sstrip,
get_nodes_all
)
from cylc.uiserver.resolvers import (
Resolvers,
list_log_files,
stream_log,
)
from cylc.uiserver.services.source_workflows import (
list_source_workflows,
get_workflow_source,
)

if TYPE_CHECKING:
from graphql import ResolveInfo
Expand Down Expand Up @@ -552,17 +562,43 @@ class UISJob(Job):
run_time = graphene.Int()


class UISQueries(Queries):
class SourceWorkflow(graphene.ObjectType):
"""A Cylc workflow source directory.

This may or may not be located within the configured cylc source
directories. For workflows located outside of the configured
directories, the "name" field will allways be null.
"""
name = graphene.String(
description='The name of the source workflow'
)
path = graphene.String(
description='The location of the source workflow.'
)
relative_path = graphene.String(
description=(
"The location of the source workflow relative to the server's CWD"
' or "null" if the source is not within CWD.'
)
)


class UISWorkflow(Workflow):
source = graphene.Field(
SourceWorkflow,
resolver=get_workflow_source,
)


class LogFiles(graphene.ObjectType):
# Example GraphiQL query:
# {
# logFiles(workflowID: "<workflow_id>", task: "<task_id>") {
# files
# }
# }
files = graphene.List(graphene.String)
class LogFiles(graphene.ObjectType):
files = graphene.List(graphene.String)


class UISQueries(Queries):
source_workflows = graphene.List(
SourceWorkflow,
resolver=list_source_workflows,
)
log_files = graphene.Field(
LogFiles,
description='List available job logs',
Expand All @@ -573,7 +609,6 @@ class LogFiles(graphene.ObjectType):
),
resolver=list_log_files
)

tasks = graphene.List(
UISTask,
description=Task._meta.description,
Expand Down Expand Up @@ -606,6 +641,20 @@ class LogFiles(graphene.ObjectType):
tasks=graphene.List(graphene.ID, default_value=[])
)

workflows = graphene.List(
UISWorkflow,
description=Workflow._meta.description,
ids=graphene.List(ID, default_value=[]),
exids=graphene.List(ID, default_value=[]),
# TODO: Change these defaults post #3500 in coordination with WUI
strip_null=graphene.Boolean(default_value=False),
delta_store=graphene.Boolean(default_value=False),
delta_type=graphene.String(default_value=DELTA_ADDED),
initial_burst=graphene.Boolean(default_value=True),
ignore_interval=graphene.Float(default_value=2.5),
resolver=get_workflows
)


class UISSubscriptions(Subscriptions):
# Example graphiql workflow log subscription:
Expand Down
15 changes: 15 additions & 0 deletions cylc/uiserver/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
120 changes: 120 additions & 0 deletions cylc/uiserver/services/source_workflows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Utilities relating to the listing and management of source workflows."""

from contextlib import suppress
from pathlib import Path
from typing import Optional, List, Dict

from cylc.flow.cfgspec.glbl_cfg import glbl_cfg
from cylc.flow.id import Tokens
from cylc.flow.network.scan import scan_multi
from cylc.flow.pathutil import get_workflow_run_dir
from cylc.flow.workflow_files import get_workflow_source_dir


# the user's configured workflow source directories
SOURCE_DIRS: List[Path] = [
Path(source_dir).expanduser()
for source_dir in glbl_cfg().get(['install', 'source dirs'])
]

CWD = Path('.').absolute()

SourceWorkflow = Dict


def _source_workflow(source_path: Path) -> SourceWorkflow:
"""Return the fields required to resolve a SourceWorkflow.

Args:
source_path:
Path to the source workflow directory.

"""
return {
'name': _get_source_workflow_name(source_path),
'path': source_path,
'relative_path': _get_source_workflow_relative_path(source_path),
}


def _blank_source_workflow() -> SourceWorkflow:
"""Return a blank source workflow.

This will be used for workflows which were not installed by "cylc install".
"""

return {'name': None, 'path': None, 'relative_path': None}


def _get_source_workflow_name(source_path: Path) -> Optional[str]:
"""Return the "name" of the source workflow.

This is the "name" that can be provided to the "cylc install" command.

Args:
source_path:
Path to the source workflow directory.

Returns:
The source workflow name if the source workflow is located within
a configured source directory, else None.

"""
for source_dir in SOURCE_DIRS:
with suppress(ValueError):
return str(source_path.relative_to(source_dir))
return None


def _get_source_workflow_relative_path(source_path: Path) -> Optional[Path]:
"""Return the relative path to the workflow from CWD.

* Returns `None` if the path is not under CWD.
* Does *not* resolve symlinks.
"""
try:
return source_path.relative_to(CWD)
except ValueError:
return None


def _get_workflow_source(workflow_id):
"""Return the source workflow for the given workflow ID."""
run_dir = get_workflow_run_dir(workflow_id)
source_dir, _symlink = get_workflow_source_dir(run_dir)
if source_dir:
return _source_workflow(Path(source_dir))
return _blank_source_workflow()


async def list_source_workflows(*_) -> List[SourceWorkflow]:
"""List source workflows located in the configured source directories."""
ret = []
async for flow in scan_multi(SOURCE_DIRS):
ret.append(_source_workflow(flow['path']))
return ret


def get_workflow_source(data, _, **kwargs) -> Optional[SourceWorkflow]:
"""Resolve the source for an installed workflow.

If the source cannot be resolved, e.g. if the workflow was not installed by
"cylc install", then this will return None.
"""
workflow_id = Tokens(data.id)['workflow']
return _get_workflow_source(workflow_id)

Check warning on line 120 in cylc/uiserver/services/source_workflows.py

View check run for this annotation

Codecov / codecov/patch

cylc/uiserver/services/source_workflows.py#L119-L120

Added lines #L119 - L120 were not covered by tests
8 changes: 8 additions & 0 deletions cylc/uiserver/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import zmq

from jupyter_server.auth.identity import User
from _pytest.monkeypatch import MonkeyPatch

from cylc.flow.cfgspec.glbl_cfg import glbl_cfg
from cylc.flow.id import Tokens
Expand Down Expand Up @@ -407,3 +408,10 @@ def _inner(cached=False):

yield _mock_glbl_cfg
rmtree(tmp_path)


@pytest.fixture(scope='module')
def mod_monkeypatch():
monkeypatch = MonkeyPatch()
yield monkeypatch
monkeypatch.undo()
14 changes: 14 additions & 0 deletions cylc/uiserver/tests/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
Loading