Skip to content

Commit

Permalink
Merge pull request #18 from wtsi-npg/devel
Browse files Browse the repository at this point in the history
Release 0.2.0
  • Loading branch information
nerdstrike authored Dec 19, 2024
2 parents 31e1371 + e046294 commit f2d0a99
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 12 deletions.
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Referenced from:
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot

version: 2
updates:

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
14 changes: 11 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
# Change Log for npg_porch Project
<!-- markdownlint-disable MD024 -->
# Change Log for npg_porch_cli Project

The format is based on [Keep a Changelog](http://keepachangelog.com/).
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
## [0.2.0] - 2024-12-18

### Added

* Added .github/dependabot.yml file to auto-update GitHub actions
* Implemented the `create_token` action. Provided the caller has an admin token,
this action generates and returns a new pipeline-specific token.
* Used npg-python-lib to read Porch config

## [0.1.0] - 2024-07-23

### Added

# Initial project scaffold, code and tests
* Initial project scaffold, code and tests
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "npg_porch_cli"
version = "0.1.0"
version = "0.2.0"
authors = [
"Marina Gourtovaia",
"Kieron Taylor",
Expand All @@ -11,11 +11,12 @@ readme = "README.md"
license = "GPL-3.0-or-later"

[tool.poetry.scripts]
npg_porch_client = "npg_porch_cli.api_cli_user:run"
npg_porch_client = "npg_porch_cli.api_cli_user:run"

[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.31.0"
npg-python-lib = { git="https://github.com/wtsi-npg/npg-python-lib", tag="0.3.4"}

[tool.poetry.dev-dependencies]
black = "^22.3.0"
Expand Down
40 changes: 37 additions & 3 deletions src/npg_porch_cli/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,18 +162,24 @@ def list_client_actions() -> list[str]:
return sorted(_PORCH_CLIENT_ACTIONS.keys())


def send(action: PorchAction, pipeline: Pipeline = None) -> dict | list:
def send(
action: PorchAction, pipeline: Pipeline = None, description: str | None = None
) -> dict | list:
"""Sends a request to the porch API server.
Sends a request to the porch API server to perform an action defined
by the `action` attribute of the `action` argument. The context of the
query is defined by the pipeline argument.
See also send_request for SSL validation guidance
Args:
action:
npg_porch_cli.api.PorchAction object
pipeline:
npg_porch_cli.api.Pipeline object
description:
A description for the new token, optional
Returns:
The server's response is returned as a Python data structure.
Expand All @@ -183,6 +189,8 @@ def send(action: PorchAction, pipeline: Pipeline = None) -> dict | list:
function = _PORCH_CLIENT_ACTIONS[action.action]
if action.action == "list_pipelines":
return function(action=action)
elif action.action == "create_token":
return function(action=action, pipeline=pipeline, description=description)
return function(action=action, pipeline=pipeline)


Expand Down Expand Up @@ -333,13 +341,39 @@ def update_task(action: PorchAction, pipeline: Pipeline):
)


def create_token(action: PorchAction, pipeline: Pipeline, description: str):
"""Creates a new token for the pipeline.
Args:
action:
npg_porch_cli.api.PorchAction object
pipeline:
npg_porch_cli.api.Pipeline object
description:
A short token description
Returns:
A dictionary containing a new token.
"""

if not description:
raise TypeError("Token description should be given")

return send_request(
validate_ca_cert=action.validate_ca_cert,
url=urljoin(action.porch_url, f"pipelines/{pipeline.name}/token/{description}"),
method="POST",
)


_PORCH_CLIENT_ACTIONS = {
"list_tasks": list_tasks,
"list_pipelines": list_pipelines,
"add_pipeline": add_pipeline,
"add_task": add_task,
"claim_task": claim_task,
"update_task": update_task,
"create_token": create_token,
}


Expand All @@ -358,8 +392,8 @@ def send_request(
Args:
validate_ca_cert:
A boolean flag defining whether the server CA certificate
will be validated. If set to True, SSL_CERT_FILE environment
variable should be set.
will be validated. If set to True, REQUESTS_CA_BUNDLE environment
variable should be set, for example to /etc/ssl/certs/ca-certificates.crt
url:
A URL to send the request to.
method:
Expand Down
13 changes: 11 additions & 2 deletions src/npg_porch_cli/api_cli_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def run():
list_tasks
list_pipelines
add_pipeline
create_token
add_task
claim_task
update_task
Expand All @@ -54,13 +55,15 @@ def run():
`--pipeline_name` is defined, `list_tasks` returns a list of tasks for
this pipeline, otherwise all registered tasks are returned.
All non-list actions require all `--pipeline`, `pipeline_url` and
All non-list actions require `--pipeline`, `pipeline_url` and
`--pipeline_version` defined.
The `add_task` and `update_task` actions require the `--task_json` to
be defined. In addition to this, for the `update_task` action `--status`
should be defined.
The `create_token` action requires that the `--description` is defined.
NPG_PORCH_TOKEN environment variable should be set to the value of
either an admin or project-specific token.
Expand Down Expand Up @@ -94,6 +97,7 @@ def run():
parser.add_argument("--pipeline", type=str, help="Pipeline name, optional")
parser.add_argument("--task_json", type=str, help="Task as JSON, optional")
parser.add_argument("--status", type=str, help="New status to set, optional")
parser.add_argument("--description", type=str, help="Token description, optional")

args = parser.parse_args()

Expand All @@ -110,4 +114,9 @@ def run():
name=args.pipeline, uri=args.pipeline_url, version=args.pipeline_version
)

print(json.dumps(send(action=action, pipeline=pipeline), indent=2))
print(
json.dumps(
send(action=action, pipeline=pipeline, description=args.description),
indent=2,
)
)
52 changes: 52 additions & 0 deletions src/npg_porch_cli/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from dataclasses import dataclass
from os import R_OK, access
from os.path import isfile

from npg.conf import IniData


@dataclass(frozen=True, kw_only=True)
class PorchClientConfig:
"""
Suggested config file content for interacting with a Porch server instance
"""

api_url: str
pipeline_name: str
pipeline_uri: str
pipeline_version: str
npg_porch_token: str


def get_config_data(
conf_file_path: str, conf_file_section: str = "PORCH"
) -> PorchClientConfig:
"""
Parses a configuration file and returns its content.
Args:
conf_file_path:
A configuration file with database connection details.
conf_file_section:
The section of the configuration file. Optional. Should be defined
for 'ini' files.
Returns:
For an 'ini' file returns the content of the given section of the file as
a dictionary.
For a 'json' file, if the conf_file_section argument is not defined, the
content of a file as a Python object is returned. If the conf_file_section
argument is defined, the object returned by the parser is assumed to be
a dictionary that has the value of the 'conf_file_section' argument as a key.
The value corresponding to this key is returned.
"""

if isfile(conf_file_path) and access(conf_file_path, R_OK):
porch_conf = IniData(PorchClientConfig).from_file(
conf_file_path, conf_file_section
)
else:
raise FileNotFoundError(f"{conf_file_path} is not present or cannot be read")

return porch_conf
17 changes: 17 additions & 0 deletions tests/data/conf.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[STUFF]

logging = INFO

[PORCH]

api_url = https://porch.dnapipelines.sanger.ac.uk
pipeline_name = test_pipeline
pipeline_uri = https://test.pipeline.com
pipeline_version = 9.9.9
npg_porch_token = 0123456789abcdef0123456789abcdef

[PARTIALPORCH]

api_url = https://porch.dnapipelines.sanger.ac.uk
pipeline_name = test_pipeline
pipeline_uri = https://test.pipeline.com
31 changes: 29 additions & 2 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def test_listing_actions():
"add_pipeline",
"add_task",
"claim_task",
"create_token",
"list_pipelines",
"list_tasks",
"update_task",
Expand All @@ -76,8 +77,8 @@ def test_porch_action_class(monkeypatch):
PorchAction(porch_url=url, action="list_tools")
assert (
e.value.args[0] == "Action 'list_tools' is not valid. "
"Valid actions: add_pipeline, add_task, claim_task, list_pipelines, "
"list_tasks, update_task"
"Valid actions: add_pipeline, add_task, claim_task, create_token, "
"list_pipelines, list_tasks, update_task"
)

pa = PorchAction(porch_url=url, action="list_tasks")
Expand Down Expand Up @@ -247,3 +248,29 @@ def mock_get_200(*args, **kwargs):
porch_url=url, action="update_task", task_input=task, task_status="DONE"
)
assert send(action=pa, pipeline=p) == response_data

with monkeypatch.context() as mkp:
response_data = {
"name": "p1",
"description": "for my pipeline",
"token": "ccceddd450aaa",
}

def mock_get_200(*args, **kwargs):
return MockPorchResponse(response_data, 200)

mkp.setattr(requests, "request", mock_get_200)

pa = PorchAction(porch_url=url, action="create_token")

error_message = "Token description should be given"
with pytest.raises(TypeError) as e:
send(action=pa, pipeline=p)
assert e.value.args[0] == error_message
with pytest.raises(TypeError) as e:
send(action=pa, pipeline=p, description="")
assert e.value.args[0] == error_message

assert (
send(action=pa, pipeline=p, description="for my pipeline") == response_data
)
17 changes: 17 additions & 0 deletions tests/test_porch_client_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pytest import raises

from npg_porch_cli.config import PorchClientConfig, get_config_data


def test_conf_obj():
config_obj = get_config_data("tests/data/conf.ini")
assert config_obj.pipeline_name == "test_pipeline"
assert config_obj.pipeline_version == "9.9.9"

assert type(config_obj) is PorchClientConfig

with raises(FileNotFoundError, match="notafile is not present or cannot be read"):
get_config_data("notafile", conf_file_section="ABSENT")

with raises(TypeError, match="missing 2 required keyword-only arguments"):
get_config_data("tests/data/conf.ini", conf_file_section="PARTIALPORCH")

0 comments on commit f2d0a99

Please sign in to comment.