Skip to content

Commit

Permalink
Refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
nx10 committed Aug 1, 2024
1 parent b546325 commit 0932ddd
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 87 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
# Styx definitions and minimal runtime
# Styx type definitions and minimal runtime

[![Build](https://github.com/childmindresearch/styxdefs/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/childmindresearch/styxdefs/actions/workflows/test.yaml?query=branch%3Amain)
[![codecov](https://codecov.io/gh/childmindresearch/styxdefs/branch/main/graph/badge.svg?token=22HWWFWPW5)](https://codecov.io/gh/childmindresearch/styxdefs)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
![stability-stable](https://img.shields.io/badge/stability-stable-green.svg)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/childmindresearch/styxdefs/blob/main/LICENSE)
[![pages](https://img.shields.io/badge/api-docs-blue)](https://childmindresearch.github.io/styxdefs)

Type definitions and minimal runtime for [Styx](https://github.com/childmindresearch/styx) generated wrappers and
Styx Runners. This package defines the common API for all Styx generated code.

## Installation

```bash
pip install styxdefs
```
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 1 addition & 31 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,56 +32,26 @@ testpaths = [
ignore_missing_imports = true

[tool.ruff]
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv"
]
line-length = 88
indent-width = 4
src = ["src"]
target-version = "py311"

[tool.ruff.lint]
select = ["ANN", "D", "E", "F", "I"]
select = ["ANN", "D", "E", "F", "I", "UP"]
ignore = [
"ANN101", # self should not be annotated.
"ANN102" # cls should not be annotated.
]
fixable = ["ALL"]
unfixable = []
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = []

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"

[build-system]
requires = ["poetry-core>=1.2.0"]
build-backend = "poetry.core.masonry.api"
10 changes: 7 additions & 3 deletions src/styxdefs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
""".. include:: ../../README.md""" # noqa: D415

from .runner import (
DefaultRunner,
from .global_state import (
get_global_runner,
set_global_runner,
)
from .local_runner import (
LocalRunner,
)
from .types import (
Execution,
InputPathType,
Metadata,
OutputPathType,
Runner,
StyxRuntimeError,
)

__all__ = [
Expand All @@ -19,7 +22,8 @@
"Metadata",
"OutputPathType",
"Runner",
"DefaultRunner",
"LocalRunner",
"get_global_runner",
"set_global_runner",
"StyxRuntimeError",
]
20 changes: 20 additions & 0 deletions src/styxdefs/global_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Global state for the Styx library."""

from .local_runner import LocalRunner
from .types import Runner

_STYX_GLOBAL_RUNNER: Runner | None = None


def get_global_runner() -> Runner:
"""Get the default runner."""
global _STYX_GLOBAL_RUNNER
if _STYX_GLOBAL_RUNNER is None:
_STYX_GLOBAL_RUNNER = LocalRunner()
return _STYX_GLOBAL_RUNNER


def set_global_runner(runner: Runner) -> None:
"""Set the default runner."""
global _STYX_GLOBAL_RUNNER
_STYX_GLOBAL_RUNNER = runner
90 changes: 48 additions & 42 deletions src/styxdefs/runner.py → src/styxdefs/local_runner.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,83 @@
"""Default runner implementation."""
"""Simple local runner implementation."""

import logging
import os
import pathlib
import shlex
from collections import deque
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from functools import partial
from subprocess import PIPE, CalledProcessError, Popen

from .types import Execution, InputPathType, Metadata, OutputPathType, Runner


class _DefaultExecution(Execution):
"""Default execution implementation."""

def __init__(self, logger: logging.Logger, dir: pathlib.Path) -> None:
from subprocess import PIPE, Popen

from .types import (
Execution,
InputPathType,
Metadata,
OutputPathType,
Runner,
StyxRuntimeError,
)


class _LocalExecution(Execution):
"""Local execution object."""

def __init__(
self,
logger: logging.Logger,
output_dir: pathlib.Path,
metadata: Metadata,
) -> None:
"""Initialize the execution."""
self.logger: logging.Logger = logger
self.dir: pathlib.Path = dir
self.output_dir: pathlib.Path = output_dir
self.metadata: Metadata = metadata

while self.dir.exists():
while self.output_dir.exists():
self.logger.warning(
f"Execution directory {self.dir} already exists. Trying another."
f"Output directory {self.output_dir} already exists. Trying another."
)
self.dir = self.dir.with_name(f"{self.dir.name}_1")
self.dir.mkdir(parents=True, exist_ok=True)
self.output_dir = self.output_dir.with_name(f"{self.output_dir.name}_1")
self.output_dir.mkdir(parents=True, exist_ok=True)

def input_file(self, host_file: InputPathType) -> str:
"""Resolve host input files."""
return str(pathlib.Path(host_file).absolute())

def output_file(self, local_file: str, optional: bool = False) -> OutputPathType:
"""Resolve local output files."""
return self.dir / local_file
return self.output_dir / local_file

def run(self, cargs: list[str]) -> None:
"""Run the command."""
self.logger.debug(f"Running command: {cargs} in '{self.dir}'.")
self.logger.debug(f"Running command: {shlex.join(cargs)}")

def _stdout_handler(line: str) -> None:
self.logger.info(line)

def _stderr_handler(line: str) -> None:
self.logger.error(line)

with Popen(cargs, text=True, stdout=PIPE, stderr=PIPE, cwd=self.dir) as process:
time_start = datetime.now()
with Popen(
cargs, text=True, stdout=PIPE, stderr=PIPE, cwd=self.output_dir
) as process:
with ThreadPoolExecutor(2) as pool: # two threads to handle the streams
exhaust = partial(pool.submit, partial(deque, maxlen=0))
exhaust(_stdout_handler(line[:-1]) for line in process.stdout) # type: ignore
exhaust(_stderr_handler(line[:-1]) for line in process.stderr) # type: ignore
return_code = process.poll()
time_end = datetime.now()
self.logger.info(f"Executed {self.metadata.name} in {time_end - time_start}")
if return_code:
raise CalledProcessError(return_code, process.args)
raise StyxRuntimeError(return_code, cargs)


class DefaultRunner(Runner):
"""Default runner implementation."""
class LocalRunner(Runner):
"""Local runner implementation."""

logger_name = "styx_default_runner"
logger_name = "styx_local_runner"

def __init__(self, data_dir: InputPathType | None = None) -> None:
"""Initialize the runner."""
Expand All @@ -77,26 +97,12 @@ def __init__(self, data_dir: InputPathType | None = None) -> None:

def start_execution(self, metadata: Metadata) -> Execution:
"""Start a new execution."""
output_dir = (
self.data_dir / f"{self.uid}_{self.execution_counter}_{metadata.name}"
)
self.execution_counter += 1
return _DefaultExecution(
return _LocalExecution(
logger=self.logger,
dir=self.data_dir
/ f"{self.uid}_{self.execution_counter - 1}_{metadata.name}",
output_dir=output_dir,
metadata=metadata,
)


_DEFAULT_RUNNER: Runner | None = None


def get_global_runner() -> Runner:
"""Get the default runner."""
global _DEFAULT_RUNNER
if _DEFAULT_RUNNER is None:
_DEFAULT_RUNNER = DefaultRunner()
return _DEFAULT_RUNNER


def set_global_runner(runner: Runner) -> None:
"""Set the default runner."""
global _DEFAULT_RUNNER
_DEFAULT_RUNNER = runner
31 changes: 31 additions & 0 deletions src/styxdefs/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Types for styx generated wrappers."""

import pathlib
import shlex
import typing

InputPathType: typing.TypeAlias = pathlib.Path | str
Expand Down Expand Up @@ -53,6 +54,10 @@ class Metadata(typing.NamedTuple):
"""Unique identifier of the tool."""
name: str
"""Name of the tool."""
package: str | None = None
"""Name of the package that provides the tool."""
citations: list[str] | None = None
"""List of references to cite when using the tool."""
container_image_type: str | None = None
"""Type of container image. Example: docker, singularity."""
container_image_tag: str | None = None
Expand Down Expand Up @@ -83,3 +88,29 @@ def start_execution(self, metadata: Metadata) -> Execution:
Called before any `Execution.input_file()` calls.
"""
...


class StyxRuntimeError(Exception):
"""Styx runtime error.
Raised when a command reports a non-zero return code.
"""

def __init__(
self,
return_code: int | None = None,
command_args: list[str] | None = None,
) -> None:
"""Initialize the error."""
self.return_code = return_code
self.command_args = command_args

message = "Command failed."

if return_code is not None:
message += f"\n- Return code: {return_code}"

if command_args is not None:
message += f"\n- Command args: {shlex.join(command_args)}"

super().__init__(message)
6 changes: 0 additions & 6 deletions tests/test_dummy.py

This file was deleted.

14 changes: 14 additions & 0 deletions tests/test_global_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Test global state."""

import styxdefs


def test_global_runner() -> None:
"""Test the global runner."""
runner = styxdefs.get_global_runner()
assert hasattr(runner, "start_execution")
assert isinstance(runner, styxdefs.LocalRunner)

styxdefs.set_global_runner(styxdefs.LocalRunner(data_dir="xyz"))
runner = styxdefs.get_global_runner()
assert isinstance(runner, styxdefs.LocalRunner)
27 changes: 27 additions & 0 deletions tests/test_local_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Test the local runner."""

import os
import pathlib

import styxdefs


def test_local_runner(tmp_path: pathlib.Path) -> None:
"""Test the local runner."""
runner = styxdefs.LocalRunner(data_dir=tmp_path / "xyz")

x = runner.start_execution(
styxdefs.Metadata(
id="123",
name="test",
)
)

input_file = x.input_file("abc")
output_file = x.output_file("def")
if os.name == "posix":
x.run(["ls"])

assert pathlib.Path(input_file).name == "abc"
assert output_file.is_relative_to(tmp_path / "xyz")
assert output_file.name == "def"
Loading

0 comments on commit 0932ddd

Please sign in to comment.