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

Release 0.2.0 #18

Merged
merged 12 commits into from
Dec 19, 2024
Merged
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")
Loading