diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 97aff48..432de85 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -60,7 +60,7 @@ representative at an online or offline event. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -ENTER_YOUR_EMAIL_ADDRESS. +33600480+nx10@users.noreply.github.com. All complaints will be reviewed and investigated promptly and fairly. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b899aff..d869d7d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# template-python-repository pull request guidelines +# styxgraph pull request guidelines -Pull requests are always welcome, and we appreciate any help you give. Note that a code of conduct applies to all spaces managed by the template-python-repository project, including issues and pull requests. Please see the [Code of Conduct](CODE_OF_CONDUCT.md) for details. +Pull requests are always welcome, and we appreciate any help you give. Note that a code of conduct applies to all spaces managed by the styxgraph project, including issues and pull requests. Please see the [Code of Conduct](CODE_OF_CONDUCT.md) for details. When submitting a pull request, we ask you to check the following: diff --git a/LICENSE b/LICENSE index 20566e6..55fdf53 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Child Mind Institute +Copyright (c) 2024 Child Mind Institute Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c8507f3..18ff3c5 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,24 @@ -[![DOI](https://zenodo.org/badge/657341621.svg)](https://zenodo.org/doi/10.5281/zenodo.10383685) - -# CMI-DAIR Template Python Repository - -Welcome to the CMI-DAIR Template Python Repository! This template is designed to streamline your project setup and ensure a consistent structure. To get started, follow these steps: - - -- [ ] Run `setup_template.py` to initialize the repository. -- [ ] Replace the content of this `README.md` with details specific to your project. -- [ ] Install the `pre-commit` hooks to ensure code quality on each commit. -- [ ] Revise SECURITY.md to reflect supported versions or remove it if not applicable. -- [ ] Remove the placeholder src and test files, these are there merely to show how the CI works. -- [ ] If it hasn't already been done for your organization/acccount, grant third-party app permissions for CodeCov. -- [ ] To set up an API documentation website, after the first successful build, go to the `Settings` tab of your repository, scroll down to the `GitHub Pages` section, and select `gh-pages` as the source. This will generate a link to your API docs. -- [ ] Update stability badge in `README.md` to reflect the current state of the project. A list of stability badges to copy can be found [here](https://github.com/orangemug/stability-badges). The [node documentation](https://nodejs.org/docs/latest-v20.x/api/documentation.html#documentation_stability_index) can be used as a reference for the stability levels. - # Project name -[![Build](https://github.com/childmindresearch/template-python-repository/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/childmindresearch/template-python-repository/actions/workflows/test.yaml?query=branch%3Amain) -[![codecov](https://codecov.io/gh/childmindresearch/template-python-repository/branch/main/graph/badge.svg?token=22HWWFWPW5)](https://codecov.io/gh/childmindresearch/template-python-repository) +[![Build](https://github.com/childmindresearch/styxgraph/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/childmindresearch/styxgraph/actions/workflows/test.yaml?query=branch%3Amain) +[![codecov](https://codecov.io/gh/childmindresearch/styxgraph/branch/main/graph/badge.svg?token=22HWWFWPW5)](https://codecov.io/gh/childmindresearch/styxgraph) [![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/template-python-repository/blob/main/LICENSE) -[![pages](https://img.shields.io/badge/api-docs-blue)](https://childmindresearch.github.io/template-python-repository) - -What problem does this tool solve? - -## Features - -- A few -- Cool -- Things +[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/childmindresearch/styxgraph/blob/main/LICENSE) +[![pages](https://img.shields.io/badge/api-docs-blue)](https://childmindresearch.github.io/styxgraph) -## Installation - -Install this package via : - -```sh -pip install APP_NAME -``` - -Or get the newest development version via: - -```sh -pip install git+https://github.com/childmindresearch/template-python-repository -``` - -## Quick start - -Short tutorial, maybe with a +## Usage ```Python -import APP_NAME +from styxdefs import set_global_runner, get_global_runner +from styxgraph import GraphRunner + +set_global_runner(DockerRunner()) # (Optional) Use any Styx runner like usual +set_global_runner(GraphRunner(get_global_runner())) # Use GraphRunner middleware -APP_NAME.short_example() -``` +# Use any Styx functions as usual +# ... -## Links or References +print(get_global_runner().mermaid()) # Print mermaid diagram -- [https://www.wikipedia.de](https://www.wikipedia.de) +``` \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md index 9fd57bf..149e386 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -15,6 +15,6 @@ Note which version(s) will receive security updates. For example: ## Reporting Vulnerabilities -To report security vulnerabilities, please do NOT use our issues page. Instead, kindly email us at ENTER_YOUR_EMAIL_ADDRESS. Please refrain from using other communication channels. +To report security vulnerabilities, please do NOT use our issues page. Instead, kindly email us at 33600480+nx10@users.noreply.github.com. Please refrain from using other communication channels. For non-security-related issues, we welcome your input and feedback on our issues page. Feel free to share your ideas and suggestions to help us improve our services. diff --git a/pyproject.toml b/pyproject.toml index 6d020c2..5c928ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [tool.poetry] -name = "app-name" +name = "styxgraph" version = "0.1.0" -description = "A beautiful description." -authors = ["Reinder Vos de Wael "] -license = "LGPL-2.1" +description = "Execution graph middleware for Styx" +authors = ["Florian Rupprecht <33600480+nx10@users.noreply.github.com>"] +license = "MIT License" readme = "README.md" -packages = [{include = "APP_NAME", from = "src"}] +packages = [{include = "styxgraph", from = "src"}] [tool.poetry.dependencies] python = "~3.11" diff --git a/setup/__init__.py b/setup/__init__.py deleted file mode 100644 index 5b41398..0000000 --- a/setup/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Module for code required by the setup_template.py script.""" diff --git a/setup/licenses.py b/setup/licenses.py deleted file mode 100644 index 6633d2b..0000000 --- a/setup/licenses.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Functions for fetching and replacing the license file.""" - -from __future__ import annotations - -import json -import re -from datetime import datetime -from typing import Optional -from urllib import request - -from setup import settings - -DIR_REPO = settings.DIR_REPO -LICENSES = settings.LICENSES - - -def get_license(name: str) -> dict[str, str]: - """Fetches the license files from GitHub. - - Args: - name: The name of the license to fetch. - - Returns: - The license text. - - """ - if name not in LICENSES: - raise ValueError(f"License {name} is not in {LICENSES}.") - with request.urlopen(f"https://api.github.com/licenses/{name}") as response: - license_info = response.read().decode() - return json.loads(license_info) - - -def modify_license_placeholder_text(selected_license: dict[str, str]) -> dict[str, str]: - """Modifies the placeholder text in the license. - - Args: - selected_license: The license to modify. - - Returns: - The modified license. - - """ - if selected_license["key"] == "mit": - license_holder = input("Who is the holder of the license? ") - current_year = datetime.now().year - selected_license["body"] = selected_license["body"].replace( - "[year]", - str(current_year), - ) - selected_license["body"] = selected_license["body"].replace( - "[fullname]", - license_holder, - ) - - return selected_license - - -def request_license() -> Optional[dict[str, str]]: - """Asks the user to select a license. - - Returns: - The path to the selected license. - - """ - print("Available licenses:") - print("\t0. No license") - for i, option in enumerate(LICENSES): - print(f"\t{i + 1}. {option}") - while True: - try: - choice = int(input("Enter the number of the license you want to use: ")) - if choice == 0: - return None - if 0 < choice <= len(LICENSES): - selected_license = get_license(LICENSES[choice - 1]) - break - raise ValueError("Invalid choice. Please try again.") - except ValueError: - print("Invalid choice. Please try again.") - - final_license = modify_license_placeholder_text(selected_license) - return final_license - - -def replace_license_badge(content: str, repo_license: Optional[dict[str, str]]) -> str: - """Replaces the license badge with the specified license. - - Args: - content: Content of any file that might contain a license badge. - repo_license: The license to replace the current license with. If None, - the current license badge will be deleted. - - Returns: - The content with the license badge replaced. - """ - if repo_license is None: - # remove line containing license badge - return re.sub( - r"\[!\[.*License\]\(https://img\.shields\.io/badge/license.*\)\]\(.*\)\n", - "", - content, - ) - # shield.io uses -- as an escape character, so we need to replace - with -- - license_name_upper = repo_license["key"].upper().replace("-", "--") - return content.replace( - "![MIT License]" "(https://img.shields.io/badge/license-MIT-blue.svg)]", - f"![{license_name_upper} License]" - f"(https://img.shields.io/badge/license-{license_name_upper}-blue.svg)]", - ) - - -def replace_license(repo_license: Optional[dict[str, str]]) -> None: - """Replaces the license file in the repository with the specified license. - - Args: - repo_license: The license to replace the current license with. If None, - the current license file will be deleted. - - """ - license_file = DIR_REPO / "LICENSE" - license_file.unlink(missing_ok=True) - - if repo_license is None: - return - - with open(license_file, "w", encoding="utf-8") as file_buffer: - file_buffer.write(repo_license["body"]) diff --git a/setup/settings.py b/setup/settings.py deleted file mode 100644 index cd8faa8..0000000 --- a/setup/settings.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Constants for the setup module.""" - -import pathlib as pl - -DIR_REPO = pl.Path(__file__).parent.parent -LICENSES = ("mit", "lgpl-2.1", "lgpl-3.0", "mpl-2.0", "unlicense") -TARGET_EXTENSIONS = { - ".py", - ".md", - ".yml", - ".yaml", - ".toml", - ".txt", -} diff --git a/setup_template.py b/setup_template.py deleted file mode 100644 index 462c3d5..0000000 --- a/setup_template.py +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin python3 -"""Setup template for Python repositories.""" - -import os -import pathlib as pl -import shutil - -from setup import licenses, settings - -DIR_REPO = settings.DIR_REPO -TARGET_EXTENSIONS = settings.TARGET_EXTENSIONS - - -def main() -> None: - """Entrypoint to the template setup script. - - This script will ask the user for details of the repository and then replace - the template values with the user input. It will also remove the setup files - and the setup directory. - """ - # Collect some data - git_uncommitted_changes = ( - os.popen(f"git -C {DIR_REPO} status -s").read().strip() != "" - ) - git_username = os.popen(f"git -C {DIR_REPO} config user.name").read().strip() - git_email = os.popen(f"git -C {DIR_REPO} config user.email").read().strip() - git_repo_name = ( - os.popen(f"git -C {DIR_REPO} remote get-url origin") - .read() - .split("/")[-1] - .split(".")[0] - .strip() - ) - - # Ask for some data - if git_uncommitted_changes: - print("You have uncommitted changes. Please commit or stash them first.") - exit(1) - repo_name = ( - input(f"Enter the name of the repository [{git_repo_name}]: ").strip() - or git_repo_name - ) - module_name = ( - input(f"Enter the name of the module [{repo_name}]: ").strip() or repo_name - ) - username = input(f"Enter your username [{git_username}]: ").strip() or git_username - email = input(f"Enter your email [{git_email}]: ").strip() or git_email - description = ( - input("Enter a short description of the project: ").strip() - or "A beautiful description." - ) - repo_license = licenses.request_license() - - # Print the data - print( - f"Using the following values:\n" - f"\tRepository name: '{repo_name}'\n" - f"\tModule name: '{module_name}'\n" - f"\tAuthor: '{username} <{email}'>\n" - f"\tDescription: '{description}'\n" - f"\tLicense: '{repo_license['name'] if repo_license else 'No license'}'", - ) - input("Press enter to continue...") - - # Replace the template values - for file in pl.Path(DIR_REPO).glob("**/*"): - if ( - not file.is_file() - or file.suffix not in TARGET_EXTENSIONS - or file.name == "setup_template.py" - ): - continue - - with open(file, encoding="utf-8") as f: - content = f.read() - - content_before = content - content = content.replace( - "- [ ] Run `setup_template.py`", - "- [x] Run `setup_template.py`", - ) - content = content.replace( - "- [ ] Update the `LICENSE`", - "- [x] Update the `LICENSE`", - ) - content = content.replace("template-python-repository", repo_name) - content = content.replace("APP_NAME", module_name) - content = content.replace("app-name", module_name) - content = content.replace("A beautiful description.", description) - content = content.replace("reinder.vosdewael@childmind.org", email) - content = content.replace("ENTER_YOUR_EMAIL_ADDRESS", email) - content = content.replace("Reinder Vos de Wael", username) - - content = licenses.replace_license_badge(content, repo_license) - content = content.replace( - "LGPL-2.1", repo_license["name"] if repo_license else "" - ) - - if content != content_before: - print(f"Updating {file.relative_to(DIR_REPO)}") - with open(file, "w", encoding="utf-8") as f: - f.write(content) - - licenses.replace_license(repo_license) - - dir_module = DIR_REPO / "src" / "APP_NAME" - if dir_module.exists(): - dir_module.rename(dir_module.parent / module_name) - - # Remove setup files - print("Removing setup files.") - setup_files = pl.Path(DIR_REPO / "setup").glob("*.py") - for setup_file in setup_files: - pl.Path(DIR_REPO / "setup" / setup_file).unlink() - if pl.Path(DIR_REPO / "setup" / "__pycache__").exists(): - # Use a more robust method to remove the cache directory - shutil.rmtree(DIR_REPO / "setup" / "__pycache__") - pl.Path(DIR_REPO / "setup").rmdir() - pl.Path(__file__).unlink() - - -if __name__ == "__main__": - main() diff --git a/src/APP_NAME/__init__.py b/src/APP_NAME/__init__.py deleted file mode 100644 index 7cff8c5..0000000 --- a/src/APP_NAME/__init__.py +++ /dev/null @@ -1 +0,0 @@ -""".. include:: ../../README.md""" # noqa: D415 diff --git a/src/APP_NAME/algorithms.py b/src/APP_NAME/algorithms.py deleted file mode 100644 index 020e25f..0000000 --- a/src/APP_NAME/algorithms.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Example functions for APP_NAME.""" - - -def fibonacci(n: int) -> int: - """Return the n-th Fibonacci number. - - Args: - n: The index of the Fibonacci number to return. - - Returns: - The n-th Fibonacci number. - """ - if n < 0: - raise ValueError("n must be non-negative") - - if int(n) != n: - raise ValueError("n must be an integer") - - if n == 0: - return 0 - if n <= 2: - return 1 - return fibonacci(n - 1) + fibonacci(n - 2) diff --git a/src/styxgraph/__init__.py b/src/styxgraph/__init__.py new file mode 100644 index 0000000..8ea7b85 --- /dev/null +++ b/src/styxgraph/__init__.py @@ -0,0 +1,66 @@ +""".. include:: ../../README.md""" # noqa: D415 + +import pathlib +from styxdefs import set_global_runner, get_global_runner, Runner, Execution, Metadata, InputPathType, OutputPathType + + +class _GraphExecution(Execution): + def __init__(self, base: Execution, graph_runner: 'GraphRunner', metadata: Metadata) -> None: + self.base = base + self.graph_runner = graph_runner + self.metadata = metadata + self.input_files = [] + self.output_files = [] + + def input_file(self, host_file: InputPathType) -> str: + self.input_files.append(host_file) + return self.base.input_file(host_file) + + def run(self, cargs: list[str]) -> None: + self.graph_runner.graph_append(self.metadata, self.input_files, self.output_files) + return self.base.run(cargs) + + def output_file(self, local_file: str, optional: bool = False) -> OutputPathType: + output_file = self.base.output_file(local_file, optional) + self.output_files.append(output_file) + return output_file + + +# Define a new runner +class GraphRunner(Runner): + def __init__(self, base: Runner): + self.base = base + self.graph: list[tuple[str, list[InputPathType], list[OutputPathType]]] = [] + + def start_execution(self, metadata: Metadata) -> Execution: + return _GraphExecution(self.base.start_execution(metadata), self, metadata) + + def graph_append(self, metadata: Metadata, input_file: list[InputPathType], output_file: list[OutputPathType]) -> None: + print(f'Appending {metadata.name} with {input_file} and {output_file}') + self.graph.append((metadata.name, input_file, output_file)) + + def mermaid(self) -> str: + connections = [] + inputs_lookup = {} + outputs_lookup = {} + for id, inputs, outputs in self.graph: + for input in inputs: + if input not in inputs_lookup: + inputs_lookup[input] = [] + inputs_lookup[input].append(id) + root_output = outputs[0] + outputs_lookup[root_output] = id + + for id, inputs, _ in self.graph: + for input in inputs: + for output, output_id in outputs_lookup.items(): + if pathlib.Path(input).is_relative_to(output): # is subfolder/file in output root + connections.append(f'{output_id} --> {id}') + + # Generate mermaid + mermaid = 'graph TD\n' + for id, _, _ in self.graph: + mermaid += f' {id}\n' + for connection in connections: + mermaid += f' {connection}\n' + return mermaid diff --git a/tests/test_dummy.py b/tests/test_dummy.py new file mode 100644 index 0000000..208542e --- /dev/null +++ b/tests/test_dummy.py @@ -0,0 +1,6 @@ +"""Dummy test.""" + + +def test_dummy() -> None: + """Test dummy.""" + assert True diff --git a/tests/test_fibonacci.py b/tests/test_fibonacci.py deleted file mode 100644 index b09e49c..0000000 --- a/tests/test_fibonacci.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Tests for the fibonacci() function.""" - -import pytest - -from APP_NAME import algorithms - - -def test_fibonacci_success_0() -> None: - """Test that fibonacci() returns the correct value for valid input.""" - output = algorithms.fibonacci(0) - - assert output == 0 - - -def test_fibonacci_success_18() -> None: - """Test that fibonacci() returns the correct value for valid input.""" - expected = 4181 - - actual = algorithms.fibonacci(19) - - assert actual == expected - - -def test_fibonacci_negative() -> None: - """Test that fibonacci() raises an exception for negative input.""" - with pytest.raises(ValueError): - algorithms.fibonacci(-1) - - -def test_fibonacci_non_integer() -> None: - """Test that fibonacci() raises an exception for non-integer input.""" - with pytest.raises(ValueError): - algorithms.fibonacci(3.14) # type: ignore[arg-type]