Skip to content

Commit 6e6bf75

Browse files
authored
Bugfix issue when nodes were not sorted when saved in a flow (jsocol#151)
* Bugfix issue when nodes were not sorted when saved in a flow * Upgrade requirements
1 parent 54f8148 commit 6e6bf75

File tree

6 files changed

+120
-59
lines changed

6 files changed

+120
-59
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ repos:
6767
additional_dependencies: [types-PyYAML, types-requests]
6868

6969
- repo: https://github.com/pycqa/pylint
70-
rev: v2.14.0-b1
70+
rev: v2.14.0
7171
hooks:
7272
- id: pylint
7373
args:

requirements-dev.txt

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# File generated automatically. Do not update it manually.
22
# Update files in the requirements folder instead.
3-
alembic==1.7.7
3+
alembic==1.8.0
44
anyio==3.6.1
55
apispec==5.2.2
66
apispec-webframeworks==0.5.2
77
asgiref==3.5.2
88
astroid==2.11.5
99
attrs==21.4.0
1010
black==22.3.0
11-
boto3==1.23.10
12-
botocore==1.26.10
11+
boto3==1.24.0
12+
botocore==1.27.0
1313
callee==0.3.1
1414
certifi==2022.5.18.1
1515
cffi==1.15.0
@@ -25,7 +25,7 @@ distlib==0.3.4
2525
factory-boy==3.2.1
2626
Faker==13.12.0
2727
fastapi==0.78.0
28-
filelock==3.7.0
28+
filelock==3.7.1
2929
Flask==2.1.2
3030
freezegun==1.2.1
3131
funcy==1.17
@@ -62,7 +62,7 @@ psycopg2-binary==2.9.3
6262
py==1.11.0
6363
pycparser==2.21
6464
pydantic==1.9.1
65-
pylint==2.13.9
65+
pylint==2.14.0
6666
pyparsing==3.0.9
6767
pytest==7.1.2
6868
pytest-cases==3.6.13
@@ -78,16 +78,17 @@ pytz==2022.1
7878
PyYAML==6.0
7979
requests==2.27.1
8080
responses==0.21.0
81-
s3transfer==0.5.2
81+
s3transfer==0.6.0
8282
sentry-sdk==1.5.12
8383
six==1.16.0
8484
sniffio==1.2.0
85-
SQLAlchemy==1.4.36
85+
SQLAlchemy==1.4.37
8686
sqlalchemy-repr==0.0.2
8787
starlette==0.19.1
8888
-e git+ssh://[email protected]/Destygo/pystatsd.git@c755b95da1f4692c49b8191f9ea0ef92f4c13c2c#egg=statsd
8989
toml==0.10.2
9090
tomli==2.0.1
91+
tomlkit==0.11.0
9192
typeguard==2.13.3
9293
typing-inspect==0.7.1
9394
typing_extensions==4.2.0

requirements.txt

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# File generated automatically. Do not update it manually.
22
# Update files in the requirements folder instead.
3-
alembic==1.7.7
3+
alembic==1.8.0
44
anyio==3.6.1
55
asgiref==3.5.2
6-
boto3==1.23.10
7-
botocore==1.26.10
6+
boto3==1.24.0
7+
botocore==1.27.0
88
certifi==2022.5.18.1
99
charset-normalizer==2.0.12
1010
click==8.1.3
@@ -34,11 +34,11 @@ python-dotenv==0.20.0
3434
python-magic==0.4.26
3535
python-multipart==0.0.5
3636
requests==2.27.1
37-
s3transfer==0.5.2
37+
s3transfer==0.6.0
3838
sentry-sdk==1.5.12
3939
six==1.16.0
4040
sniffio==1.2.0
41-
SQLAlchemy==1.4.36
41+
SQLAlchemy==1.4.37
4242
sqlalchemy-repr==0.0.2
4343
starlette==0.19.1
4444
-e git+ssh://[email protected]/Destygo/pystatsd.git@c755b95da1f4692c49b8191f9ea0ef92f4c13c2c#egg=statsd

tests/adapters/flow_validation/test_flow_validation_success.py

+69-26
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,48 @@
11
# pylint: disable=redefined-outer-name
2+
from random import Random
3+
from uuid import UUID
4+
5+
import pytest
6+
import pytest_cases
7+
28
from tests.adapters.flow_validation import helpers
3-
from tests.fixtures.valid_flow_json import * # noqa: F401, F403, pylint: disable=wildcard-import,unused-wildcard-import
9+
from tests.fixtures.valid_flow_json import * # noqa: F403, pylint: disable=wildcard-import,unused-wildcard-import
410
from use_case_executor.adapters.flow_validation.flow import Flow
511
from use_case_executor.domain.flow.edge_data.edge_type import EdgeType
612
from use_case_executor.domain.flow.node_data.answer_data.answer_data import ButtonType
713

814

9-
def test_working_flow_is_loaded( # pylint: disable=too-many-locals
10-
*,
15+
def _shuffled(objects: list, random_instance: Random) -> list:
16+
objects = list(objects)
17+
random_instance.shuffle(objects)
18+
return objects
19+
20+
21+
@pytest.fixture
22+
def shuffled_reference_flow(reference_flow: dict) -> dict:
23+
random_instance = Random(123)
24+
return {
25+
**reference_flow,
26+
"nodes": _shuffled(reference_flow["nodes"], random_instance=random_instance),
27+
"edges": _shuffled(reference_flow["edges"], random_instance=random_instance),
28+
}
29+
30+
31+
@pytest.fixture
32+
def inverted_reference_flow(reference_flow: dict) -> dict:
33+
return {
34+
**reference_flow,
35+
"nodes": reference_flow["nodes"][::-1],
36+
"edges": reference_flow["edges"][::-1],
37+
}
38+
39+
40+
@pytest_cases.parametrize(
41+
"flow_dict",
42+
[reference_flow, shuffled_reference_flow, inverted_reference_flow], # noqa: F405
43+
)
44+
@pytest.mark.parametrize("convert_into_domain_object", [False, True])
45+
def test_working_flow_is_loaded( # pylint: disable=too-many-locals,too-many-arguments
1146
reference_flow,
1247
start_node_uuid,
1348
button_uuid,
@@ -33,16 +68,20 @@ def test_working_flow_is_loaded( # pylint: disable=too-many-locals
3368
edge_to_second_user_input_uuid,
3469
edge_to_create_ticket_uuid,
3570
edge_to_empty_node_uuid,
71+
flow_dict,
72+
convert_into_domain_object,
3673
):
37-
flow = Flow.load_flow(reference_flow)
74+
flow = Flow.load_flow(flow_dict)
75+
if convert_into_domain_object:
76+
flow = Flow.from_domain_object(domain_flow=flow.to_domain_object())
77+
3878
assert flow.schema_version == "v0.2"
3979
assert flow.start_node_uuid == start_node_uuid
4080

41-
nodes = flow.nodes
42-
assert len(nodes) == 10
43-
81+
assert len(flow.nodes) == 10
82+
nodes_per_uuid = {node.uuid: node for node in flow.nodes}
4483
helpers.check_answer_node(
45-
node=nodes[0],
84+
node=nodes_per_uuid[UUID(reference_flow["nodes"][0]["uuid"])],
4685
node_uuid=start_node_uuid,
4786
bubbles=reference_flow["nodes"][0]["data"]["bubbles"],
4887
buttons=[
@@ -66,17 +105,17 @@ def test_working_flow_is_loaded( # pylint: disable=too-many-locals
66105
is_deflection=True,
67106
)
68107
helpers.check_go_to_flow_node(
69-
node=nodes[1],
108+
node=nodes_per_uuid[UUID(reference_flow["nodes"][1]["uuid"])],
70109
node_uuid=go_to_flow_uuid,
71110
target_use_case_uuid=target_use_case_uuid,
72111
)
73112
helpers.check_go_to_advanced_node(
74-
node=nodes[2],
113+
node=nodes_per_uuid[UUID(reference_flow["nodes"][2]["uuid"])],
75114
node_uuid=go_to_advanced_uuid,
76115
target_intent_record_id=target_intent_record_id,
77116
)
78117
helpers.check_answer_node(
79-
node=nodes[3],
118+
node=nodes_per_uuid[UUID(reference_flow["nodes"][3]["uuid"])],
80119
node_uuid=second_answer_node_uuid,
81120
bubbles=reference_flow["nodes"][3]["data"]["bubbles"],
82121
buttons=[],
@@ -86,23 +125,23 @@ def test_working_flow_is_loaded( # pylint: disable=too-many-locals
86125
is_deflection=False,
87126
)
88127
helpers.check_agent_handover_node(
89-
node=nodes[4],
128+
node=nodes_per_uuid[UUID(reference_flow["nodes"][4]["uuid"])],
90129
node_uuid=agent_handover_uuid,
91130
agent_channel_uuid=agent_handover_channel_uuid,
92131
handover_success_message_translations={"en_US": "Handover success text"},
93132
)
94133
helpers.check_user_input_node(
95-
node=nodes[5],
134+
node=nodes_per_uuid[UUID(reference_flow["nodes"][5]["uuid"])],
96135
node_uuid=user_input_uuid,
97136
node_data_dict=reference_flow["nodes"][5]["data"],
98137
)
99138
helpers.check_user_input_node(
100-
node=nodes[6],
139+
node=nodes_per_uuid[UUID(reference_flow["nodes"][6]["uuid"])],
101140
node_uuid=second_user_input_uuid,
102141
node_data_dict=reference_flow["nodes"][6]["data"],
103142
)
104143
helpers.check_answer_node(
105-
node=nodes[7],
144+
node=nodes_per_uuid[UUID(reference_flow["nodes"][7]["uuid"])],
106145
node_uuid=third_answer_node_uuid,
107146
bubbles=reference_flow["nodes"][7]["data"]["bubbles"],
108147
buttons=[],
@@ -111,16 +150,20 @@ def test_working_flow_is_loaded( # pylint: disable=too-many-locals
111150
has_go_back_quick_reply=True,
112151
is_deflection=False,
113152
)
114-
helpers.check_empty_node(node=nodes[8], node_uuid=empty_node_uuid)
153+
helpers.check_empty_node(
154+
node=nodes_per_uuid[UUID(reference_flow["nodes"][8]["uuid"])],
155+
node_uuid=empty_node_uuid,
156+
)
115157
helpers.check_create_ticket_node(
116-
node=nodes[9],
158+
node=nodes_per_uuid[UUID(reference_flow["nodes"][9]["uuid"])],
117159
node_uuid=create_ticket_node_uuid,
118160
node_data_dict=reference_flow["nodes"][9]["data"],
119161
)
120162

121163
assert len(flow.edges) == 9
164+
edges_per_uuid = {edge.data.uuid: edge for edge in flow.edges}
122165
helpers.check_quick_reply_edge(
123-
edge=flow.edges[0],
166+
edge=edges_per_uuid[UUID(reference_flow["edges"][0]["data"]["uuid"])],
124167
source=start_node_uuid,
125168
target=go_to_flow_uuid,
126169
data={
@@ -133,7 +176,7 @@ def test_working_flow_is_loaded( # pylint: disable=too-many-locals
133176
},
134177
)
135178
helpers.check_quick_reply_edge(
136-
edge=flow.edges[1],
179+
edge=edges_per_uuid[UUID(reference_flow["edges"][1]["data"]["uuid"])],
137180
source=start_node_uuid,
138181
target=go_to_advanced_uuid,
139182
data={
@@ -146,7 +189,7 @@ def test_working_flow_is_loaded( # pylint: disable=too-many-locals
146189
},
147190
)
148191
helpers.check_quick_reply_edge(
149-
edge=flow.edges[2],
192+
edge=edges_per_uuid[UUID(reference_flow["edges"][2]["data"]["uuid"])],
150193
source=start_node_uuid,
151194
target=second_answer_node_uuid,
152195
data={
@@ -159,7 +202,7 @@ def test_working_flow_is_loaded( # pylint: disable=too-many-locals
159202
},
160203
)
161204
helpers.check_quick_reply_edge(
162-
edge=flow.edges[3],
205+
edge=edges_per_uuid[UUID(reference_flow["edges"][3]["data"]["uuid"])],
163206
source=start_node_uuid,
164207
target=agent_handover_uuid,
165208
data={
@@ -170,7 +213,7 @@ def test_working_flow_is_loaded( # pylint: disable=too-many-locals
170213
},
171214
)
172215
helpers.check_quick_reply_edge(
173-
edge=flow.edges[4],
216+
edge=edges_per_uuid[UUID(reference_flow["edges"][4]["data"]["uuid"])],
174217
source=start_node_uuid,
175218
target=user_input_uuid,
176219
data={
@@ -181,7 +224,7 @@ def test_working_flow_is_loaded( # pylint: disable=too-many-locals
181224
},
182225
)
183226
helpers.check_quick_reply_edge(
184-
edge=flow.edges[5],
227+
edge=edges_per_uuid[UUID(reference_flow["edges"][5]["data"]["uuid"])],
185228
source=start_node_uuid,
186229
target=second_user_input_uuid,
187230
data={
@@ -194,7 +237,7 @@ def test_working_flow_is_loaded( # pylint: disable=too-many-locals
194237
},
195238
)
196239
helpers.check_quick_reply_edge(
197-
edge=flow.edges[6],
240+
edge=edges_per_uuid[UUID(reference_flow["edges"][6]["data"]["uuid"])],
198241
source=start_node_uuid,
199242
target=empty_node_uuid,
200243
data={
@@ -205,7 +248,7 @@ def test_working_flow_is_loaded( # pylint: disable=too-many-locals
205248
},
206249
)
207250
helpers.check_quick_reply_edge(
208-
edge=flow.edges[7],
251+
edge=edges_per_uuid[UUID(reference_flow["edges"][7]["data"]["uuid"])],
209252
source=start_node_uuid,
210253
target=create_ticket_node_uuid,
211254
data={
@@ -216,7 +259,7 @@ def test_working_flow_is_loaded( # pylint: disable=too-many-locals
216259
},
217260
)
218261
helpers.check_direct_link_edge(
219-
flow.edges[8],
262+
edge=edges_per_uuid[UUID(reference_flow["edges"][8]["data"]["uuid"])],
220263
source=user_input_uuid,
221264
target=third_answer_node_uuid,
222265
data={"type": EdgeType.direct_link, "uuid": edge_to_third_answer_uuid},

use_case_executor/adapters/flow_validation/flow.py

+11-20
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from typing import Any
44
from uuid import UUID
55

6-
import funcy
76
import marshmallow_dataclass
87
import more_itertools
98
from marshmallow import Schema
@@ -222,27 +221,19 @@ def to_domain_object(self) -> DomainFlow:
222221
start_node_type=start_node.type,
223222
start_node_data=start_node.data.to_domain_object(),
224223
)
225-
226-
# As each node except the start node must have exactly one parent,
227-
# then iterating on all nodes and creating their parent edge for each of them
228-
# is enough to create all nodes and all edges in the domain flow.
229-
incoming_edges_per_node_uuid: dict[UUID, list[Edge]] = funcy.group_by(
230-
lambda edge: edge.target, self.edges
231-
)
232224
for node in self.nodes:
233-
if node.uuid == start_node.uuid:
234-
continue
235-
236-
incoming_edges = incoming_edges_per_node_uuid.get(node.uuid, [])
237-
incoming_edge: Edge = more_itertools.one(incoming_edges)
238-
domain_flow.add_node(
239-
parent_node=domain_flow.get_node(uuid=incoming_edge.source),
240-
node_uuid=node.uuid,
241-
node_type=node.type,
242-
node_data=node.data.to_domain_object(),
243-
incoming_edge_data=incoming_edge.data.to_domain_object(),
225+
if node.uuid != start_node.uuid:
226+
domain_flow.add_orphan_node(
227+
node_uuid=node.uuid,
228+
node_type=node.type,
229+
node_data=node.data.to_domain_object(),
230+
)
231+
for edge in self.edges:
232+
domain_flow.assign_parent_to_orphan_node(
233+
node=domain_flow.get_node(uuid=edge.target),
234+
parent_node=domain_flow.get_node(uuid=edge.source),
235+
incoming_edge_data=edge.data.to_domain_object(),
244236
)
245-
246237
return domain_flow
247238

248239
@classmethod

use_case_executor/domain/flow/flow.py

+26
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,32 @@ def add_node(
5757
parent_node.add_edge(data=incoming_edge_data, target_node=node)
5858
return node
5959

60+
def add_orphan_node(
61+
self,
62+
node_uuid: UUID,
63+
node_type: NodeType,
64+
node_data: NodeData,
65+
) -> Node:
66+
assert node_uuid not in self._node_per_uuid
67+
node = Node(
68+
uuid=node_uuid,
69+
type=node_type,
70+
data=node_data,
71+
parent_node_uuid=None,
72+
outgoing_edges=[],
73+
)
74+
self._node_per_uuid[node_uuid] = node
75+
return node
76+
77+
def assign_parent_to_orphan_node(
78+
self, node: Node, parent_node: Node, incoming_edge_data: EdgeData
79+
) -> None:
80+
assert node.uuid in self._node_per_uuid
81+
assert parent_node.uuid in self._node_per_uuid
82+
assert node.parent_node_uuid is None
83+
node.parent_node_uuid = parent_node.uuid
84+
parent_node.add_edge(data=incoming_edge_data, target_node=node)
85+
6086
def get_nodes(self) -> list[Node]:
6187
return list(self._node_per_uuid.values())
6288

0 commit comments

Comments
 (0)