Skip to content

Commit d981742

Browse files
Merge pull request #35 from transhapHigsn/feat/choice_flow_2
Feat/choice flow 2
2 parents 63140a2 + 88716da commit d981742

File tree

8 files changed

+511
-57
lines changed

8 files changed

+511
-57
lines changed

README.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# freak
1+
# Freak
22

33
<div align="center">
44

@@ -33,10 +33,17 @@ Since, we have a basic understanding of what exactly a flow in freak is, let's m
3333

3434
- **Butler** is responsible for reading python modules, locating steps and organizing them using locator specified by flow.
3535

36-
- **Executor** is core component of the engine. It is responsible for executing steps. Currently, it only supports linear flows.
36+
- **Executor** is core component of the engine. It is responsible for executing steps. Currently, it only supports linear and choice-based flows.
3737

3838
- **Inspector** is used to return input schema for every step defined by the flow. This is intended to be part of view logic of the engine.
3939

40+
## Supported flows
41+
42+
- Linear Flows
43+
- Choice Flows
44+
45+
## Sample Code
46+
4047
**Freak** is currently under active development. Following code should give you an idea how it implements data flows.
4148

4249
This is how you define a flow using base flow.
@@ -117,10 +124,11 @@ def func_four(ctx: RequestContext) -> Response:
117124
Following test case will use above defintion to execute the flow.
118125

119126
```python
120-
from freak.engine import Engine
127+
from freak.provider import EngineProvider
121128

122129
def test_base_flow_prosecutioner():
123-
executioner = Engine(module_name=__name__, decorator_name="base_flow")
130+
engine = EngineProvider(flow_name="base_flow").engine
131+
executioner = engine(module_name=__name__, decorator_name="base_flow")
124132

125133
response = executioner.execute(data={"a": 4, "b": 7}, from_step="func_one")
126134

@@ -183,10 +191,11 @@ def test_base_flow_prosecutioner():
183191
Using above code, it is also possible to generate input schema for every step. Following test case will demonstrate this behaviour.
184192

185193
```python
186-
from freak.engine import Engine
194+
from freak.provider import EngineProvider
187195

188196
def test_base_flow_fetch_schema():
189-
executioner = Engine(module_name=__name__, decorator_name="base_flow")
197+
engine = EngineProvider(flow_name="base_flow").engine
198+
executioner = engine(module_name=__name__, decorator_name="base_flow")
190199
responses = executioner.inspect()
191200

192201
input_model_b_schema = {
@@ -214,10 +223,14 @@ def test_base_flow_fetch_schema():
214223
assert input_model_schema == InputModel.schema()
215224
assert input_model_b_schema == InputModelB.schema()
216225

217-
assert responses[0]["schema"] == input_model_schema
218-
assert responses[1]["schema"] == input_model_schema
219-
assert responses[2]["schema"] == input_model_schema
220-
assert responses[3]["schema"] == input_model_b_schema
226+
assert executioner.flow.predecessor == responses["graph"]
227+
228+
schema_info = responses["schema"]
229+
230+
assert schema_info["func_one"]["schema"] == input_model_schema
231+
assert schema_info["func_two"]["schema"] == input_model_schema
232+
assert schema_info["func_three"]["schema"] == input_model_schema
233+
assert schema_info["func_four"]["schema"] == input_model_b_schema
221234
```
222235

223236
<!-- ## Very first steps

freak/engine.py

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from inspect import getabsfile
66

77
from freak.models.request import FetchSchemaRequestContext, RequestContext
8-
from freak.models.response import EngineResponse
8+
from freak.models.response import EngineResponse, Response
99
from freak.types import LOCATOR_TYPE, Flow, Step
1010

1111

@@ -51,6 +51,13 @@ def get_step(self, from_step: Optional[str]) -> Step:
5151
raise Exception("InvalidStepError")
5252
return step
5353

54+
def get_next_step_uid(
55+
self, resp_ctx: Response, next_steps: List[str]
56+
) -> str:
57+
assert len(next_steps) == 1
58+
59+
return next_steps[0]
60+
5461
def get_following_steps(
5562
self, from_step: Optional[str], path_traversed: Dict[str, Any]
5663
) -> List[str]:
@@ -111,7 +118,9 @@ def execute(
111118
if not next_steps:
112119
break
113120

114-
next_step_uid = next_steps[0]
121+
next_step_uid = self.get_next_step_uid(
122+
resp_ctx=resp_ctx, next_steps=next_steps
123+
)
115124

116125
next_steps = self.get_following_steps(
117126
from_step=next_step_uid,
@@ -129,43 +138,21 @@ def execute(
129138
path_traversed,
130139
)
131140

132-
def inspect(
133-
self,
134-
from_step: Optional[str] = None,
135-
) -> List[Dict[str, Any]]:
136-
responses = []
137-
path_traversed: Dict[str, Any] = {"traversed": {}, "last_step": ""}
141+
def inspect(self) -> Dict[str, Any]:
142+
steps = self.flow.predecessor
143+
steps_arr = []
144+
[steps_arr.extend(value) for value in steps.values()] # type: ignore
138145

139-
step = self.get_step(from_step=from_step)
140-
next_steps = self.get_following_steps(
141-
from_step=from_step,
142-
path_traversed=path_traversed,
143-
)
144-
145-
while True:
146+
final_response = {}
147+
for _step in steps_arr:
148+
step = self.get_step(from_step=_step)
146149
ctx = FetchSchemaRequestContext(name=step.name, order=step.order)
147150

148151
resp_ctx = step.function(ctx=ctx) # type: ignore
149-
responses.append(
150-
{
151-
"name": step.name,
152-
"order": step.order,
153-
"schema": resp_ctx.output["schema"],
154-
}
155-
)
156-
157-
path_traversed["traversed"][step.uid] = next_steps
158-
path_traversed["last_step"] = step.uid
159-
160-
if not next_steps:
161-
break
162-
163-
next_step_uid = next_steps[0]
164-
165-
next_steps = self.get_following_steps(
166-
from_step=next_step_uid,
167-
path_traversed=path_traversed,
168-
)
169-
step = self.get_step(from_step=next_step_uid)
152+
final_response[_step] = {
153+
"name": step.name,
154+
"order": step.order,
155+
"schema": resp_ctx.output["schema"],
156+
}
170157

171-
return responses
158+
return {"graph": steps, "schema": final_response}

freak/flows/choice_flow.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""
2+
Note: Do not remove base_flow and locator import.
3+
"""
4+
5+
6+
from typing import Any, Dict, List, Optional, Tuple
7+
8+
import copy
9+
10+
from freak.engine import Engine
11+
from freak.flows.base_flow import base_flow as choice_flow
12+
from freak.flows.base_flow import locator
13+
from freak.models.request import RequestContext
14+
from freak.models.response import EngineResponse, Response
15+
16+
17+
class ChoiceFlowEngine(Engine):
18+
def __init__(
19+
self, module_name: str, decorator_name: str = "choice_flow"
20+
) -> None:
21+
super().__init__(module_name=module_name, decorator_name=decorator_name)
22+
23+
def get_following_steps(
24+
self, from_step: Optional[str], path_traversed: Dict[str, Any]
25+
) -> List[str]:
26+
step_graph = self.flow.predecessor
27+
if not from_step:
28+
# pick up root of flow.
29+
from_step = step_graph[from_step][0]
30+
31+
next_steps = step_graph.get(from_step, [])
32+
33+
last_step = path_traversed.get("last_step", "")
34+
if from_step not in step_graph.get(last_step, []) and last_step:
35+
raise Exception("CannotExecuteError")
36+
37+
return next_steps
38+
39+
def get_next_step_uid(
40+
self, resp_ctx: Response, next_steps: List[str]
41+
) -> str:
42+
if resp_ctx.choice:
43+
if resp_ctx.choice not in next_steps:
44+
raise Exception("InvalidChoice")
45+
46+
return resp_ctx.choice
47+
48+
assert len(next_steps) == 1
49+
50+
return next_steps[0]
51+
52+
def execute(
53+
self,
54+
from_step: Optional[str],
55+
data: Dict[str, Any],
56+
executed_steps: Dict[str, Any] = {"traversed": {}, "last_step": ""},
57+
) -> Tuple[EngineResponse, Dict[str, Any]]:
58+
path_traversed = copy.deepcopy(executed_steps)
59+
60+
step = self.get_step(from_step=from_step)
61+
next_steps = self.get_following_steps(
62+
from_step=from_step,
63+
path_traversed=path_traversed,
64+
)
65+
66+
responses = []
67+
last_successful_step, to_step, from_ = (
68+
step.order,
69+
step.order,
70+
step.order,
71+
)
72+
73+
while True:
74+
ctx = RequestContext(input=data, name=step.name, order=step.order)
75+
76+
resp_ctx = step.function(ctx=ctx) # type: ignore
77+
responses.append(resp_ctx)
78+
to_step = step.order # this will refer to last performed step.
79+
if not resp_ctx.success:
80+
break
81+
82+
path = next_steps.copy()
83+
if next_steps:
84+
next_step_uid = self.get_next_step_uid(
85+
resp_ctx=resp_ctx, next_steps=next_steps
86+
)
87+
path = [next_step_uid]
88+
89+
path_traversed["traversed"][step.uid] = path
90+
path_traversed["last_step"] = step.uid
91+
92+
data = resp_ctx.input
93+
94+
# this will refer last successfully performed action.
95+
last_successful_step = step.order
96+
97+
if not next_steps:
98+
break
99+
100+
next_step_uid = self.get_next_step_uid(
101+
resp_ctx=resp_ctx, next_steps=next_steps
102+
)
103+
104+
next_steps = self.get_following_steps(
105+
from_step=next_step_uid,
106+
path_traversed=path_traversed,
107+
)
108+
step = self.get_step(from_step=next_step_uid)
109+
110+
return (
111+
EngineResponse(
112+
responses=responses,
113+
from_step=from_,
114+
to_step=to_step,
115+
last_successful_step=last_successful_step,
116+
),
117+
path_traversed,
118+
)

freak/models/input.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@ class InputModelB(BaseModel):
2424
a: int
2525
b: int
2626
c: int
27+
28+
29+
class InputModelC(BaseModel):
30+
"""Class for defining structure of request data."""
31+
32+
a: int
33+
b: int
34+
d: int

freak/models/response.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, List
1+
from typing import Any, Dict, List, Optional
22

33
import abc
44
import json
@@ -12,6 +12,7 @@ class Response(abc.ABC):
1212
messages: List[str]
1313
output: Dict[str, Any]
1414
success: bool
15+
choice: Optional[str]
1516

1617

1718
class SuccessResponseContext(Response):
@@ -21,9 +22,15 @@ class SuccessResponseContext(Response):
2122
json_errors: str = ""
2223
messages: List[str] = []
2324

24-
def __init__(self, input: Dict[str, Any], output: Dict[str, Any]):
25+
def __init__(
26+
self,
27+
input: Dict[str, Any],
28+
output: Dict[str, Any],
29+
choice: Optional[str] = None,
30+
):
2531
self.input = input
2632
self.output = output
33+
self.choice = choice
2734

2835

2936
class ErrorResponseContext(Response):
@@ -32,6 +39,7 @@ class ErrorResponseContext(Response):
3239
output: Dict[str, Any] = {}
3340
success: bool = False
3441
json_errors: str = ""
42+
choice: Optional[str] = None
3543

3644
def __init__(self, input: Dict[str, Any], messages: List[str]) -> None:
3745
self.input = input
@@ -43,6 +51,7 @@ class InputErrorsResponseContext(Response):
4351
messages: List[str] = []
4452
output: Dict[str, Any] = {}
4553
success: bool = False
54+
choice: Optional[str] = None
4655

4756
def __init__(self, input: Dict[str, Any], json_errors: str):
4857
self.input = input
@@ -66,6 +75,7 @@ class FetchInputSchemaContext(Response):
6675
success: bool = field(default=True)
6776
json_errors: str = field(default="")
6877
input: Dict[str, Any] = {"fetch_schema": True}
78+
choice: Optional[str] = None
6979

7080
def __init__(self, output: Dict[str, Any]):
7181
self.output = output

freak/provider.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import Any
2+
3+
import inspect
4+
from importlib import import_module
5+
6+
7+
class EngineProvider:
8+
def __init__(self, flow_name: str) -> None:
9+
look_here = f"freak.flows.{flow_name}"
10+
module = import_module(name=look_here)
11+
self.engine = self.find_engine(module=module)
12+
13+
def find_engine(self, module: object) -> Any:
14+
from freak.engine import Engine
15+
16+
for _, cls in inspect.getmembers(module, inspect.isclass):
17+
if issubclass(cls, Engine) and cls != Engine:
18+
return cls
19+
else:
20+
21+
return Engine

0 commit comments

Comments
 (0)