arkfbp 0.0.3
Python implementation of the arkfbp
License: MIT
Requires Python: !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*,
Keywords: arkfbp

# arkfbp-py

arkfbp-py is the python implementation of the arkfbp.

# installation

arkfbp-py需要 Python 3.6+ 及Django 2.0+ 的版本支持。

    pip3 install arkfbp (暂不可用)
    
    or
    
    pip3 install git+https://github.com/longguikeji/arkfbp-py.git@zzr/basic

# Dev installation

    python3 setup.py install
    
# Quick Start

1、新建名为`demo`的项目:

    arkfbp-py startproject demo

2、在项目根目录下，新建名为`app1`的应用:

    arkfbp-py startapp app1

3、移动到`demo/app1/flows`目录下，新建名为`flow1`的流，并设置类型 --class:

    arkfbp-py createflow flow1 --class view
 
4、移动到`demo/app1/flows/flow1/nodes`目录下，新建名为`node1`的节点,并设置类型 --class和标识 --id:

    arkfbp-py createnode node1 --class function --id node1

5、在`Node1`的`run`方法示例如下:

        def run(self, *args, **kwargs):
            print(f'Hello, Node1!')
            return 'hello arkfbp'

6、`demo/app1/flows/flow1`的`main.py`示例如下:
    
    from arkfbp.node import StartNode, StopNode
    from arkfbp.graph import Graph
    # Editor your flow here.
    from arkfbp.flow import ViewFlow
    from app1.flows.flow1.nodes.node1 import Node1


    class Main(ViewFlow):

        def create_nodes(self):
            return [
                {
                    'cls': StartNode,
                    'id': 'start',
                    'next': 'node1'
                },
                {
                    'cls': Node1,
                    'id': 'node1',
                    'next': 'stop'
                },
                {
                    'cls': StopNode,
                    'id': 'stop'
                }
            ]

7、在`demo/arkfbp/routes/demo.json`中配置路由信息:
    
    {
        "namespace": "demo/v1/",
        "routes": [
            {
                "flow1/": {
                    "get": "app1.flows.flow1"
                }
            }
        ]
    }

8、迁移路由信息，其中参数`--topdir`可指定路由配置信息所在目录，参数`--urlfile`可指定迁移后的文件所在路径，默认会在项目settings.py文件所在路径查找并生成文件:

    python3 manage.py migrateroute --topdir demo --urlfile demo/demo_urls.py

9、将`8`中生成的url文件，配置到项目的demo/urls.py中。
    
    from django.contrib import admin
    from django.urls import path, include

    urlpatterns = [
        path('admin/', admin.site.urls),
        path('', include('demo.demo_urls'))
    ]

10、尝试运行流`flow1`:

    python3 manage.py runflow --flow app1.flows.flow1.main --input {\"username\": \"admin\"} --http_method post --header {\"Authorization\": \"token\"}

11、使用`django`原生方式启动`server`。
    
    python3 manage.py runserver 0.0.0.0:8000

# Advanced usage

## GlobalHookFlow（已废弃）

全局钩子式工作流运行的场景适用于：

1）服务进行路由之前（self.before_route）

2）所有工作流运行之前（self.before_flow）

3）所有工作流运行之后（self.after_flow）

4）抛出异常之前（self.before_exception）

### 简单使用

1、创建全局钩子式工作流，在项目根目录创建`hook.py`文件(仅为示例)

    from arkfbp.flow import GlobalHookFlow
    class HookFlow(GlobalHookFlow):
    
        def create_nodes(self):
            return [
                {
                    'cls': StartNode,
                    'id': 'start',
                    'next': 'stop'
                },
                {
                    'cls': StopNode,
                    'id': 'stop'
                }
            ]
    
        def set_mount(self):
            self.before_flow = True

2、在`set_mount()`方法中设置想要开启钩子的位置。
    
    def set_mount(self):
        """
        设置为在所有工作流运行之前执行全局钩子流
        """
        self.before_flow = True

3、将钩子流配置到项目的`settings.py`文件的`MIDDLEWARE`变量中。

    INSTALLED_APPS = [
        ...
    ]
    MIDDLEWARE = [
        ...
        'hook.HookFlow',
        'hook.HookFlow1',
        'hook.HookFlow2',
    ]

### HookFlow的执行顺序

`GlobalHookFlow`的执行顺序与`django`原生`Middleware`执行顺序一致，
before_route()、before_flow()的执行顺序依次为从上至下；after_flow()、before_exception()则为从下至上。

## New GlobalHookFlow

全新的钩子流现已可以使用。

### 简单使用

1、在demo/hook/文件夹下创建一个全局钩子流，并设置类型 --class。

    arkfbp-py createflow hook1 --class view

2、创建节点Node1（过程略），并编辑。
    
    class Node1(FunctionNode):

    id = 'node1'

    def run(self, *args, **kwargs):
        print(f'Hello, Hook!')
        return None

3、在demo/arkfbp/hooks/hook.json中设置流的执行位置。
    
    {
        "before_route": ["hook.hook1"],
        "before_flow": [],
        "before_exception": [],
        "before_response": []
    }
4、这样在每次路由之前，都会先进入hook1这个流进行处理。

### 详解

全局钩子式工作流运行的场景适用于：

1）接口路由之前（before_route）

2）工作流运行之前（before_flow）

3）返回响应之前（before_response）

4）抛出异常之前（before_exception）

列表中流的摆放顺序，即为执行顺序。

## Flow Hook

1、流创建成功后

    def created(inputs, *args, **kwargs):
        pass

2、流初始化之前

    def before_initialize(inputs, *args, **kwargs):
        pass
            
3、流初始化之后

    def initialized(inputs, *args, **kwargs):
        pass
    
4、流执行之前

    def before_execute(inputs, *args, **kwargs):
        pass
        
5、流执行之后

    def executed(inputs, ret, *args, **kwargs):
        pass
 
6、流被销毁之前

    def before_destroy(inputs, ret, *args, **kwargs):
        pass

## ShutDown Flow

### Flow Shutdown
现在，你可以通过`flow.shutdown(outputs, **kwargs)`方法，来随时随地的停止工作流的运行

如果你使用`ViewFlow`来定义流，那么可指定返回的`response`的状态码`response_status`，例如：

    class Main(ViewFlow):

        def create_nodes(self):
            return [
                {
                    'cls': StartNode,
                    'id': 'start',
                    'next': 'node1'
                },
                {
                    'cls': Node1,
                    'id': 'node1',
                    'next': 'stop'
                },
                {
                    'cls': StopNode,
                    'id': 'stop'
                }
            ]
        
        def before_initialize(inputs, *args, **kwargs):
            self.shutdown('Flow Error！', response_status=400)

### Node Shutdown
同样，你也可以通过`node.flow.shutdown(outputs, **kwargs)`方法，来随时随地的停止工作流的运行。

如果你使用`ViewFlow`来定义流，那么可指定返回的`response`的状态码`response_status`，例如：
    
    class Node1(FunctionNode):

    id = 'node1'

    def run(self, *args, **kwargs):
        print(f'Hello, Hook 1!')
        self.flow.shutdown('Flow Error！', response_status=400)

## Flow State


## Flow Steps

`flow.steps`为一个`dict`，其中包含以`node_id`为`key`、以`node_instance`为`value`的数据。

现在你可以在任何一个节点，从`node.state.steps`中，获取指定的已运行的`node`。

    node1 = node.state.steps.get('node1', None)

## ViewFlow inputs

`ViewFlow`的`inputs`为原生的`django`的`WSGIRequest`对象，`ViewFlow`在此基础上为`inputs`对象增加了`data`、`extra_data`、`str`属性。

### DataSet

`ds`属性将原生`WSGIRequest`对象的`GET`和`POST`的数据合并为一个`dict`。

### extra_ds

你可以在`extra_ds`中存放你想要传递下去的任何数据。

### str

`str`包含了请求体中的字符串信息。

_注意：你可以随意为inputs增加任何属性，例如：_
    
    inputs.attr = {}

_这样你就为`inputs`增加了`attr`的属性_


## Feature For CLI

### Create Flow

现在你可以通过指定目录和基类来创建一个工作流，`--topdir`参数代表创建流的所在目录，`--class`参数代表工作流期望继承的基类流。

    python3 manage.py createflow flow1 --topdir demo/flows --class base
    
    或者
    
    arkfbp-py createflow flow1 --topdir demo/flows --class base 

详解：--class 参数可选值如下

    {
        'base': 'Flow',
        'view': 'ViewFlow',
        'hook': 'GlobalHookFlow',
    }
    
也可通过命令行获取相关信息

    arkfbp-py createflow -h

### Create Node

现在你可以通过指定目录和基类来创建一个流节点，`--topdir`参数代表创建节点的所在目录，`--class`参数代表节点期望继承的基类节点, `--id`参数代表节点在流中的唯一标识。

    python3 manage.py createnode node1 --topdir demo/flows/flow1/nodes --class base --id node1
    
    或者
    
    arkfbp-py createnode node1 --topdir demo/flows/flow1/nodes --class base --id node1

详解：--class 参数可选值如下

    {
        'base': 'Node',
        'start': 'StartNode',
        'stop': 'StopNode',
        'function': 'FunctionNode',
        'if': 'IFNode',
        'loop': 'LoopNode',
        'nop': 'NopNode',
        'api': 'APINode',
        'test': 'TestNode',
        'trigger_flow': 'TriggerFlowNode',
    }

也可通过命令行获取相关信息

    arkfbp-py createnode -h


## TestFlow

### Create Flow     

1、 通过`Quick Start`中的第3步新建一个工作流，新建的工作流的名称必须以`test`开头。 
2、 将该工作流`main.py`模块里`Main`函数的父类`ViewFlow`修改为`Flow`。  
3、 将`from arkfbp.flow import ViewFlow`修改为`from arkfbp.flow import Flow`。  
这样就得到一个测试流     
测试流的`main.py`如下：         

    from arkfbp.flow import Flow
    from arkfbp.node import StartNode, StopNode
    from app1.flows.testt1.nodes.node1 import Node1

    # Editor your flow here.
    class Main(Flow):

        def create_nodes(self):
            return [
                {
                    'cls': StartNode,
                    'id': 'start',
                    'next': 'node1'
                },{
                    'cls': Node1,
                    'id': 'node1',
                    'next': 'stop'
                },{
                    'cls': StopNode,
                    'id': 'stop'
                }
            ]     
### Create node

1、 通过`Quick Start`中的第4步新建一个节点。 
2、 将新建节点对应`python`文件里节点类的父类`FunctionNode`改为`TestNode`。   
3、 新建节点对应`python`文件里`from arkfbp.node import FunctionNode`修改为`from arkfbp.node import TestNode`。    
这样就得到一个测试节点     
测试节点`node1`如下：

    from arkfbp.node import TestNode

    # Editor your node here.
    class Node1(TestNode):

        def run(self, *args, **kwargs):
            print(f'Hello, Node1!')

### 测试节点使用      

1、 `setUp`函数    
测试节点的`setUp`函数将在测试用例执行之前调用，可用于准备数据等。      

    def setUp(self):
        print('before start test')

2、 `tearDown`函数    
测试节点的`tearDown`函数在测试用例全部执行之后调用。    

    def tearDown(self):
        print('after finish test')

3、 测试用例    
测试用例为以`test_`开头的函数。    

    def test_one(self):
        pass

4、 断言   
测试节点支持`python`自带断言和`django unittest`的断言方法。    

    def test_one(self):
        assert 1==1
    def test_two(self):
        self.assertEqual(1,1)   

5、 调用其他测试流   
在一个测试用例中可以调用其他测试流，得到被调用测试流的结果。调用方式如下：    

    from arkfbp.node import TestNode
    from app1.flows.testt1.main import Main

    class Node1(TestNode):

        def test_other_testflow(self):
            self.get_outputs(Main(),inputs={},http_method='get')

首先需要先从被调用测试流的`main`模块中引入`Main`类，然后调用函数`get_outputs`。       
函数`get_outputs`有三个参数，第一个参数为被调用测试流`Main`类的实例，即`Main()`；第二个参数为输入的数据，字典类型；第三个参数为调用测试流的方法，为`get`     

### Run Flow   

#### 运行指定目录下测试流     

1、 在项目目录下新建`python` 文件       
2、 引入`executer`模块     
3、 调用函数`start_testflows`运行测试流        
函数`start_testflows`有一个参数，表示指定的目录，传入相对路径、绝对路径均可。运行指定工作流如下：        

    from arkfbp import executer

    print(executer.FlowExecuter.start_testflows('./app1/flows/'))

若想运行全部测试流也可通过命令实现。在`manage.py`文件所在目录下输入命令`python3 manage.py flowtest`，即可直接运行所有测试流  

## Extension CLI

此部分内容适用于可视化插件开发相关人员

### AddNode

在流的图定义（create_nodes）中同步一个已知的节点信息。

    python3 manage.py ext_addnode --flow <flow_name> --class <node_class> --id <node_id> --next <next_node_id> --alias <node_alias> --x <coord_x> --y <coord_y>

#### 示例

    python3 manage.py ext_addnode --flow app1.flows.flow1 --class app1.flows.flow1.nodes.node1.Node1 --id node1 --next node2 --alias Flow1_Node1 --x 123.123456 --y 123.123456

如果使用`arkfbp-py`命令，需指定`--topdir`参数，其代表项目的绝对根路径：
    
    arkfbp-py ext_addnode --flow app1.flows.flow1 --class app1.flows.flow1.nodes.node1.Node1 --id node1 --next node2 --alias Flow1_Node1 --x 123.123456 --y 123.123456 --topdir /Users/user/Development/demo

#### 详解

参数`flow`代表流的路径以`.`分隔，具体到流的文件夹名称；参数`id`代表节点的唯一标识；参数`class`代表相关节点的路径以`.`分隔，具体到类名；参数`next`代表后继节点的`id`；参数`alias`代表在`import`时，指定的节点类的别名；参数`x`和`y`分别代表插件中的`x`、`y`坐标。
参数`id`、`flow`和`class`是必选，其他可选，不选则默认参数为`None`，你也可通过命令行获取相关信息：

    arkfbp-py ext_addnode -h

### UpdateNode

在流的图定义（create_nodes）中修改一个已知的节点信息。

    python3 manage.py ext_updatenode --flow <flow_name> --class <node_class> --id <node_id> --next <next_node_id> --alias <node_alias> --x <coord_x> --y <coord_y>

如果使用`arkfbp-py`命令，需指定`--topdir`参数，其代表项目的绝对根路径：
    
    arkfbp-py ext_updatenode --flow app1.flows.flow1 --class app1.flows.flow1.nodes.node2.Node2 --id node1 --next node3 --alias Flow1_Node2 --x 123.123456 --y 123.123456 --topdir /Users/user/Development/demo

#### 详解

参数`flow`代表流的路径以`.`分隔，具体到流的文件夹名称；参数`id`代表目标节点的唯一标识，用于指定修改的目标节点；参数`class`代表节点类型，其路径以`.`分隔并具体到类名，用于修改目标节点的类型；参数`next`代表后继节点的`id`，用于修改目标节点的后继节点；参数`alias`代表在`import`时，指定的节点类的别名，用于修改目标节点的类型别名；参数`x`和`y`分别代表插件中的`x`、`y`坐标，用于修改目标节点在插件中的坐标。
当你想要将`next`设置为`None`的时候，可以在传递参数时指定`--next`为`undefined`即可。
参数`id`、`flow`是必选，其他可选，不选则默认不更改相应参数。你也可通过命令行获取相关信息：

    arkfbp-py ext_updatenode -h

### RemoveNode

在流的图定义（create_nodes）中删除一个已知的节点信息，并自动更新前驱后继节点的连接信息。

    python3 manage.py ext_removenode --flow <flow_name> --id <node_id>

如果使用`arkfbp-py`命令，需指定`--topdir`参数，其代表项目的绝对根路径：
    
    arkfbp-py ext_removenode --flow app1.flows.flow1 --id node1 --topdir /Users/user/Development/demo

#### 详解

参数`flow`代表流的路径以`.`分隔，具体到流的文件夹名称；参数`id`代表目标节点的唯一标识，用于指定删除的目标节点；
参数`id`、`flow`是必选，其他可选。你也可通过命令行获取相关信息：

    arkfbp-py ext_removenode -h

## special usages

### csrf
若想局部禁用或模拟csrf，只需要重写指定flow的Main Class的dispatch方法。示例如下：

    from arkfbp.flow import ViewFlow
    from arkfbp.node import StartNode, StopNode
    from django.views.decorators.csrf import csrf_exempt

    class Main(ViewFlow):
        def create_nodes(self):
            return [{
                'cls': StartNode,
                'id': 'start',
                'next': 'stop',
                'x': None,
                'y': None
            }，
            {
                'cls': StopNode,
                'id': 'stop',
                'next': None,
                'x': None,
                'y': None
            }]

        @csrf_exempt
        def dispatch(self, request, *args, **kwargs):
            return super(Main, self).dispatch(request, *args, **kwargs)

### AuthTokenNode

现在可以使用AuthTokenNode来快速搭建您的用户名+密码验证流程，示例如下：
    
    from arkfbp.node import AuthTokenNode

    class VerifyPassword(AuthTokenNode):
    
        def get_ciphertext(self):
            return 'ciphertext'

        def before_execute(self, *args, **kwargs):
            self.username_field = 'USERNAME'
            self.password_field = 'PASSWORD'

#### 详解
其中，`get_ciphertext()`用于自定义从存储后端获取加密的数据；`get_key()`可自定义返回的`token`值，默认为生成一个新的`token`值；
你也可以通过`before_execute()`等`run()`方法运行前的钩子来自定义`username_field`和`password_field`来指定获取账号名和账号密码的字段名称；
`AuthTokenNode`在`run()`运行后默认返回一个长度为40的`token`字符串。

# Auto-generated code

## 编辑 meta-config

meta-config最外层结构如下：
    
    {
      "name": "",
      "type": "",
      "module": {},
      "meta": {},
      "permission": {},
      "api": {}
    }
   
### name
meta_config的名称，唯一标识（推荐和文件名相同）。
    
    {                           
      "name": "meta_config_name"
    }

### type
前端组件类型。
    
    {                           
      "type": “table"           
    }

### module
model类及meta文件的具体路径。

      "module": {
        "user": {
          "model": "arkid_meta.models.user.User"
        },
        "util": {
          "meta": "automation.util"
        }
      }

### permission
权限校验相关的路径。

    {
      "permission": {
        "role": "demo.permission.role"
      }
    }

其中role表示别名即命名空间，demo.permission.role指定的为role相关的meta config的JSON文件，实例如下：

    {
      "admin": {
        "title": "管理员",
        "flow": "demo.permission.role.admin"
      }
    }

其中admin为权限角色名称，title为权限名字，flow指定了具体校验时需要运行的工作流。

#### 使用方法
在api配置中增加permission字段来标识需要用到的permission。

    {
      "api": {
        "user/": {
          "post": {
            "name": "新建用户",
            "type": "create",
            "request": {},
            "response": {},
            "permission": ["role.admin"]  # role为上述的命名空间，admin为文件中指定的admin角色。
          }
        }
      }
    }

### meta
包含了model所有的字段信息及校验规则，书写方式分为module导入，或者自定义。

    {                        
      "meta": {              
        "field_1": {         
          "title": "title_1",
          "type": {          
            "field_type": {} 
          }                  
        }                
      }                      
    }  

#### field_1
展示的字段名称，并不代表model中原始的字段名称。

#### title_1
字段的名称，用于前端展示。

#### field_type
字段的类型，目前支持string、integer、float、object、array。

    {                        
      "meta": {              
        "field_1": {         
          "title": "title_1",
          "required": true, # 必须接受此参数
          "type": {          
            "string": {
              "read_only"：false， # 只读
              "write_only"：true，# 只写
              "min_length": 10, # 字符串最小的长度
              "max_length": 50, # 字符串最大的长度 
            } 
          }                  
        }                
      }                      
    }  

##### object field type

    "field": {
      "title": "title",
      "type": {
        "object": {
            "field_1": "field_1",
            "field_2": "field_2",
            "field_3": "field_3",
          }
      }
    }

#### array object type

    "field": {
      "title": "查询结果列表",
      "type": {
        "array": {
          "array_item": "field_1"
        }
      }
    }

### api
接口定义。

    "meta_name/<index>/": { # url，index为位置参数
      "get": { # 接口的请求方法
        "name": "update_meta_name", # 接口的名称
        "type": "retrieve", # 接口的默认类型
        "index": { # 位置参数的配置
          "id": { # 位置参数名称
          "src": "model_user.id" # 配置来源
          }
        },
        "pagination": { # 分页配置
          "enabled": true, # 是否启用
          "page_size_query_param": "page_size", # 传参的key名称，页面大小
          "page_query_param": "page", # 传参的key名称，页码
          "count_param": "count", # 记录总数的名称
          "results_param": "results", # 结果的名称
          "next_param": "next", # 下一页的名称
          "previous_param": "previous", # 上一页的名称
          "paginated_response": "utils.custom_response" # 自定义分页response，需清楚具体pagination node的response实现
        },
        "request": {}, # 接口需要接收的字段
        "response": { # 接口需要返回的字段
          "data": "items", # 表示本地meta中的配置
          "error_code": "util.error_code", # 表示从module导入的配置
          "error_message": "util.error_message" # 表示从module导入的配置
        },
        "debug": false # 是否输出debug信息，默认为true
      },
      "delete": {
        "index": "index",
        "name": "delete_meta_name",
        "http_method": "delete",
        "request": [],
        "response": []
      }
    }

#### pagination response
若想自定义分页的数据结构，你需要用到.pagination内置用法来重构响应的数据结构。

    {"meta":    
      "data": {
        "required": false,
        "type": {
          "object": {
            "total": ".pagination.count",
            "page": ".pagination.page",
            "page_size": ".pagination.page_size",
            "items": "items"
          }
        }
      }
    }

#### permission flow
在api描述中定义`permission`并引入`role`字段中定义的角色,
其中`admin.flow`是用于校验权限的工作流，其输出值为布尔类型。
    
    {
      "role": {
        "admin": {
          "title": "管理员",
          "flow": "flows.flow"
        }
      },
      "api": {
        "user/": {
          "get": {
            "name": "获取信息",
            "type": "retrieve",
            "request": {},
            "response": {},
            "debug": false,
            "permission": ["admin"]
          }
        }
      }
    }

在开启系统默认
#### custom type for api
除了create、update、retrieve、delete四种系统提供的基本的数据处理引擎，你还可以进行自定义引擎的配置。
此时不需要指定response参数。

    "custom/": {
      "post": {
        "name": "custom_1",
        "type": "custom",
        "flow": "flows.flow_1", # 指定自定义流的位置
        "request": {}, # 接口需要接收的字段
      }
    }

详解：自定义流运行之前系统会根据request中的参数先进行数据校验，
之后将validate的_data及原始的request传给自定义的flow

## 配置meta_config
将meta_config文件与django结合，以达到自动生成项目的效果。

### 编写JSON文件
将所有的meta_config统一存放到项目的某一文件夹下。

    demo
    |_ automation
      |_ meta_1.json 
      |_ ...
      |_ meta_n.json

### 配置url
在django项目的主urls.py文件中增加一条路由

    from django.contrib import admin
    from django.urls import path, include
    from arkfbp.common.automation.core import MetaConfigs
    
    meta_dir = '/demo/automation'
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('arkfbp-admin/', include(MetaConfigs(meta_dir).get_urls()))
    ]

### 运行项目
    
    python manage.py runserver