pyapp-flow


Namepyapp-flow JSON
Version 0.17 PyPI version JSON
download
home_pagehttps://github.com/pyapp-org/pyapp-flow
SummaryApplication workflow framework
upload_time2024-11-27 01:35:10
maintainerNone
docs_urlNone
authorTim Savage
requires_python<4.0,>=3.10
licenseBSD-3-Clause
keywords framework application
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # pyapp-flow
A simple application level workflow library.

Allows complex processes to be broken into smaller specific steps, greatly 
simplifying testing and re-use.

[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=pyapp-org_pyapp-flow&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=pyapp-org_pyapp-flow)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=pyapp-org_pyapp-flow&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=pyapp-org_pyapp-flow)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=pyapp-org_pyapp-flow&metric=coverage)](https://sonarcloud.io/summary/new_code?id=pyapp-org_pyapp-flow)
[![Once you go Black...](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)


## Installation

```shell
pip install pyapp-flow
```


## Usage

```python
from pathlib import Path
from typing import Sequence
import pyapp_flow as flow

# Define steps:

@flow.step(name="Load Names", output="names")
def load_names(root_path: Path) -> Sequence[str]:
    """
    Read a sequence of names from a file
    """
    with (root_path / "names.txt").open() as f_in:
        return [name.strip() for name in f_in.readlines()]

@flow.step(name="Say hello")
def say_hi(name: str):
    print(f"Hello {name}")

# Define a workflow:

great_everybody = (
    flow.Workflow(name="Great everybody in names file")
    .nodes(
      load_names,
      flow.ForEach("name", in_var="names").loop(say_hi)
    )
)

# Execute workflow:

context = flow.WorkflowContext(root_path=Path())
great_everybody(context)
```

All nodes within the workflow follow a simple interface of:
```python
def node_function(context: flow.WorkflowContext):
    ...
```
or using typing
```python
NodeFunction = Callable[[flow.WorkflowContext], Any]
```

The `step` decorator simplifies definition of a step by handling loading and saving 
of state variables from the `WorkflowContext`.


## Reference

### Workflow

At the basic level a workflow is an object that holds a series of nodes to be called 
in sequence. The workflow object also includes helper methods to generate and append
the nodes defined in the *Builtin Nodes* section of the documentation. 

Just like every node in pyApp-Flow a workflow is called with an `WorkflowContext` 
object, this means workflows can be nested in workflows, or called from a for-each 
node.

The one key aspect with a workflow object is related to context variable scope. 
When a workflow is triggered the context scope is copied and any changes made 
to the variables are discarded when the workflow ends. However, just like Python 
scoping only the reference to the variable is copied meaning mutable objects can 
be modified (eg list/dicts).

```python
workflow = (
    flow.Workflow(name="My Workflow")
    .nodes(...)
)
```

### WorkflowContext

The workflow context object holds the state of the workflow including handling 
variable scoping and helper methods for logging progress.

**Properties**

- `state` 

  Direct access to state variables in the current scope.

- `depth` 
 
  Current scope depth

- `indent` 

  Helper that returns a string indent for use formatting messages

**Methods**

- `format`

  Format a string using values from the context state. Most *name*
  values for nodes/workflows use this method to allow values to be included
  from scope eg:

  ```python
  context.format("Current path {working_path}")
  ```

- `push_state`/`pop_state`

  Used to step into or out of a lower state scope. Typically these methods are
  not called directly but are called via using a with block eg:
  
  ```python
  with context:
      pass  # Separate variable scope 
  ```

- Logging wrappers

  Wrappers around an internal workflow logger that handle indentation to make
  reading the log easier.
  
  - log
  - debug
  - info
  - warning
  - error
  - exception



### Builtin Nodes

**Modify context variables**

- `SetVar`
  
    Set one or more variables into the context

    ```python
    SetVar(my_var="foo")
    ```

- `Append`

    Append a value to a list in the context object (will create the list if it 
    does not exist).

    ```python
    Append("messages", "Operation failed to add {my_var}")
    ```
  
- `CaptureErrors`

    Capture and store any errors raised by node(s) within the capture block to a 
    variable within the context.

    ```python
    CaptureErrors("errors").nodes(my_flaky_step)
    ```
  
    This node also has a `try_all` argument that controls the behaviour when an  
    error is captured, if `True` every node is called even if they all raise errors,
    this is useful for running a number of separate tests that may fail.

    ```python
    CaptureErrors(
        "errors", 
        try_all=True
    ).nodes(
        my_first_check, 
        my_second_check, 
    )
    ```

**Provide feedback**

- `LogMessage`
    
    Insert a message within optional values from the context into the runtime 
    log with an optional level.
    
    ```python
    LogMessage("State of my_var is {my_var}", level=logging.INFO)
    ```


**Branching**

Branching nodes utilise a fluent interface for defining the nodes within each 
branch. 

- `Conditional` / `If`
    
    Analogous with an `if` statement, it can accept either a context variable 
    that can be interpreted as a `bool` or a function/lamba that accepts a 
    `WorkflowContext` object and returns a `bool`.

    ```python 
    # With context variable
    (
        If("is_successful")
        .true(log_message("Process successful :)"))
        .false(log_message("Process failed :("))
    )
  
    # With Lambda
    (
        If(lambda context: len(context.state.errors) == 0)
        .true(log_message("Process successful :)"))
        .false(log_message("Process failed :("))
    )
    ```
  
- `Switch`

    Analogous with a `switch` statement found in many languages or with Python 
    a `dict` lookup with a default fallback.

    Like the conditional node switch can accept a context variable or a 
    function/lambda that accepts a `WorkflowContext`, except returns any *hashable*
    object.

    ```python
    # With context variable
    (
        Switch("my_var")
        .case("foo", log_message("Found foo!"))
        .case("bar", log_message("Found bar!"))
        .default(log_message("Found neither."))
    )
  
    # With Lambda
    (
        Switch(lambda context: context.state["my_var"])
        .case("foo", log_message("Found foo!"))
        .case("bar", log_message("Found bar!"))
    )
    ```
  

**Iteration**

- `ForEach`
    
    Analogous with a `for` loop this node will iterate through a sequence and 
    call each of the child nodes.

    All nodes within a for-each loop are in a nested context scope.
    
    ```python
    # With a single target variable
    (
        ForEach("message", in_var="messages")
        .loop(log_message("- {message}"))
    )
  
    # With multiple target variables
    (
        ForEach("name, age", in_var="students")
        .loop(log_message("- {name} is {age} years old."))
    )
    ```


            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/pyapp-org/pyapp-flow",
    "name": "pyapp-flow",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.10",
    "maintainer_email": null,
    "keywords": "framework, application",
    "author": "Tim Savage",
    "author_email": "tim@savage.company",
    "download_url": "https://files.pythonhosted.org/packages/e8/3a/add5cee3feff5b99b042edf9dc063ca34317dbbc7e3f3abcd427fddb5d83/pyapp_flow-0.17.tar.gz",
    "platform": null,
    "description": "# pyapp-flow\nA simple application level workflow library.\n\nAllows complex processes to be broken into smaller specific steps, greatly \nsimplifying testing and re-use.\n\n[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=pyapp-org_pyapp-flow&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=pyapp-org_pyapp-flow)\n[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=pyapp-org_pyapp-flow&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=pyapp-org_pyapp-flow)\n[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=pyapp-org_pyapp-flow&metric=coverage)](https://sonarcloud.io/summary/new_code?id=pyapp-org_pyapp-flow)\n[![Once you go Black...](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)\n\n\n## Installation\n\n```shell\npip install pyapp-flow\n```\n\n\n## Usage\n\n```python\nfrom pathlib import Path\nfrom typing import Sequence\nimport pyapp_flow as flow\n\n# Define steps:\n\n@flow.step(name=\"Load Names\", output=\"names\")\ndef load_names(root_path: Path) -> Sequence[str]:\n    \"\"\"\n    Read a sequence of names from a file\n    \"\"\"\n    with (root_path / \"names.txt\").open() as f_in:\n        return [name.strip() for name in f_in.readlines()]\n\n@flow.step(name=\"Say hello\")\ndef say_hi(name: str):\n    print(f\"Hello {name}\")\n\n# Define a workflow:\n\ngreat_everybody = (\n    flow.Workflow(name=\"Great everybody in names file\")\n    .nodes(\n      load_names,\n      flow.ForEach(\"name\", in_var=\"names\").loop(say_hi)\n    )\n)\n\n# Execute workflow:\n\ncontext = flow.WorkflowContext(root_path=Path())\ngreat_everybody(context)\n```\n\nAll nodes within the workflow follow a simple interface of:\n```python\ndef node_function(context: flow.WorkflowContext):\n    ...\n```\nor using typing\n```python\nNodeFunction = Callable[[flow.WorkflowContext], Any]\n```\n\nThe `step` decorator simplifies definition of a step by handling loading and saving \nof state variables from the `WorkflowContext`.\n\n\n## Reference\n\n### Workflow\n\nAt the basic level a workflow is an object that holds a series of nodes to be called \nin sequence. The workflow object also includes helper methods to generate and append\nthe nodes defined in the *Builtin Nodes* section of the documentation. \n\nJust like every node in pyApp-Flow a workflow is called with an `WorkflowContext` \nobject, this means workflows can be nested in workflows, or called from a for-each \nnode.\n\nThe one key aspect with a workflow object is related to context variable scope. \nWhen a workflow is triggered the context scope is copied and any changes made \nto the variables are discarded when the workflow ends. However, just like Python \nscoping only the reference to the variable is copied meaning mutable objects can \nbe modified (eg list/dicts).\n\n```python\nworkflow = (\n    flow.Workflow(name=\"My Workflow\")\n    .nodes(...)\n)\n```\n\n### WorkflowContext\n\nThe workflow context object holds the state of the workflow including handling \nvariable scoping and helper methods for logging progress.\n\n**Properties**\n\n- `state` \n\n  Direct access to state variables in the current scope.\n\n- `depth` \n \n  Current scope depth\n\n- `indent` \n\n  Helper that returns a string indent for use formatting messages\n\n**Methods**\n\n- `format`\n\n  Format a string using values from the context state. Most *name*\n  values for nodes/workflows use this method to allow values to be included\n  from scope eg:\n\n  ```python\n  context.format(\"Current path {working_path}\")\n  ```\n\n- `push_state`/`pop_state`\n\n  Used to step into or out of a lower state scope. Typically these methods are\n  not called directly but are called via using a with block eg:\n  \n  ```python\n  with context:\n      pass  # Separate variable scope \n  ```\n\n- Logging wrappers\n\n  Wrappers around an internal workflow logger that handle indentation to make\n  reading the log easier.\n  \n  - log\n  - debug\n  - info\n  - warning\n  - error\n  - exception\n\n\n\n### Builtin Nodes\n\n**Modify context variables**\n\n- `SetVar`\n  \n    Set one or more variables into the context\n\n    ```python\n    SetVar(my_var=\"foo\")\n    ```\n\n- `Append`\n\n    Append a value to a list in the context object (will create the list if it \n    does not exist).\n\n    ```python\n    Append(\"messages\", \"Operation failed to add {my_var}\")\n    ```\n  \n- `CaptureErrors`\n\n    Capture and store any errors raised by node(s) within the capture block to a \n    variable within the context.\n\n    ```python\n    CaptureErrors(\"errors\").nodes(my_flaky_step)\n    ```\n  \n    This node also has a `try_all` argument that controls the behaviour when an  \n    error is captured, if `True` every node is called even if they all raise errors,\n    this is useful for running a number of separate tests that may fail.\n\n    ```python\n    CaptureErrors(\n        \"errors\", \n        try_all=True\n    ).nodes(\n        my_first_check, \n        my_second_check, \n    )\n    ```\n\n**Provide feedback**\n\n- `LogMessage`\n    \n    Insert a message within optional values from the context into the runtime \n    log with an optional level.\n    \n    ```python\n    LogMessage(\"State of my_var is {my_var}\", level=logging.INFO)\n    ```\n\n\n**Branching**\n\nBranching nodes utilise a fluent interface for defining the nodes within each \nbranch. \n\n- `Conditional` / `If`\n    \n    Analogous with an `if` statement, it can accept either a context variable \n    that can be interpreted as a `bool` or a function/lamba that accepts a \n    `WorkflowContext` object and returns a `bool`.\n\n    ```python \n    # With context variable\n    (\n        If(\"is_successful\")\n        .true(log_message(\"Process successful :)\"))\n        .false(log_message(\"Process failed :(\"))\n    )\n  \n    # With Lambda\n    (\n        If(lambda context: len(context.state.errors) == 0)\n        .true(log_message(\"Process successful :)\"))\n        .false(log_message(\"Process failed :(\"))\n    )\n    ```\n  \n- `Switch`\n\n    Analogous with a `switch` statement found in many languages or with Python \n    a `dict` lookup with a default fallback.\n\n    Like the conditional node switch can accept a context variable or a \n    function/lambda that accepts a `WorkflowContext`, except returns any *hashable*\n    object.\n\n    ```python\n    # With context variable\n    (\n        Switch(\"my_var\")\n        .case(\"foo\", log_message(\"Found foo!\"))\n        .case(\"bar\", log_message(\"Found bar!\"))\n        .default(log_message(\"Found neither.\"))\n    )\n  \n    # With Lambda\n    (\n        Switch(lambda context: context.state[\"my_var\"])\n        .case(\"foo\", log_message(\"Found foo!\"))\n        .case(\"bar\", log_message(\"Found bar!\"))\n    )\n    ```\n  \n\n**Iteration**\n\n- `ForEach`\n    \n    Analogous with a `for` loop this node will iterate through a sequence and \n    call each of the child nodes.\n\n    All nodes within a for-each loop are in a nested context scope.\n    \n    ```python\n    # With a single target variable\n    (\n        ForEach(\"message\", in_var=\"messages\")\n        .loop(log_message(\"- {message}\"))\n    )\n  \n    # With multiple target variables\n    (\n        ForEach(\"name, age\", in_var=\"students\")\n        .loop(log_message(\"- {name} is {age} years old.\"))\n    )\n    ```\n\n",
    "bugtrack_url": null,
    "license": "BSD-3-Clause",
    "summary": "Application workflow framework",
    "version": "0.17",
    "project_urls": {
        "Homepage": "https://github.com/pyapp-org/pyapp-flow",
        "Repository": "https://github.com/pyapp-org/pyapp-flow"
    },
    "split_keywords": [
        "framework",
        " application"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "e88f25d2bd2c8f8c061b795a3e1498d8872cb574bc3bd4f5e087aa0c35f80090",
                "md5": "409c13d59fcacb01962a85d3164793ec",
                "sha256": "a22c826b64c0512f9eb9c5c65b1ce7708853495796185f5ec25b9ccf38c90c52"
            },
            "downloads": -1,
            "filename": "pyapp_flow-0.17-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "409c13d59fcacb01962a85d3164793ec",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.10",
            "size": 27888,
            "upload_time": "2024-11-27T01:35:08",
            "upload_time_iso_8601": "2024-11-27T01:35:08.807711Z",
            "url": "https://files.pythonhosted.org/packages/e8/8f/25d2bd2c8f8c061b795a3e1498d8872cb574bc3bd4f5e087aa0c35f80090/pyapp_flow-0.17-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "e83aadd5cee3feff5b99b042edf9dc063ca34317dbbc7e3f3abcd427fddb5d83",
                "md5": "29250fb2df991c79fd9b247058e60813",
                "sha256": "b641a1cfb4157b861defcf02c25b81be5d17926e837004167f676151e57fa813"
            },
            "downloads": -1,
            "filename": "pyapp_flow-0.17.tar.gz",
            "has_sig": false,
            "md5_digest": "29250fb2df991c79fd9b247058e60813",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.10",
            "size": 32841,
            "upload_time": "2024-11-27T01:35:10",
            "upload_time_iso_8601": "2024-11-27T01:35:10.645582Z",
            "url": "https://files.pythonhosted.org/packages/e8/3a/add5cee3feff5b99b042edf9dc063ca34317dbbc7e3f3abcd427fddb5d83/pyapp_flow-0.17.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-11-27 01:35:10",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "pyapp-org",
    "github_project": "pyapp-flow",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "pyapp-flow"
}
        
Elapsed time: 0.44024s