pytestflow 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. bootstrap_templates/__init__.py +0 -0
  2. bootstrap_templates/config.yaml +13 -0
  3. bootstrap_templates/custom_step_types/__init__.py +0 -0
  4. bootstrap_templates/custom_step_types/custom_step_template.py +7 -0
  5. bootstrap_templates/process_models/__init__.py +0 -0
  6. bootstrap_templates/process_models/reporting/README.md +48 -0
  7. bootstrap_templates/process_models/reporting/__init__.py +0 -0
  8. bootstrap_templates/process_models/reporting/default_report.html.j2 +122 -0
  9. bootstrap_templates/process_models/reporting/html_report.py +331 -0
  10. bootstrap_templates/process_models/sequential_model.py +141 -0
  11. bootstrap_templates/test_sequences/__init__.py +0 -0
  12. bootstrap_templates/test_sequences/basic_sequence.py +62 -0
  13. bootstrap_templates/test_sequences/message_box_and_flow_control.py +169 -0
  14. bootstrap_templates/test_sequences/motherboard_test_sequence.py +125 -0
  15. bootstrap_templates/test_sequences/step_types_quickstart.py +168 -0
  16. pytestflow/README.md +13 -0
  17. pytestflow/__init__.py +19 -0
  18. pytestflow/backend/__init__.py +2 -0
  19. pytestflow/backend/event_bus.py +27 -0
  20. pytestflow/backend/frontend/assets/full_logo-D1DRTUt8.svg +21 -0
  21. pytestflow/backend/frontend/assets/index-480TOyh4.js +2 -0
  22. pytestflow/backend/frontend/assets/index-qEI3VAQU.css +1 -0
  23. pytestflow/backend/frontend/index.html +14 -0
  24. pytestflow/backend/frontend/logo.svg +21 -0
  25. pytestflow/backend/handlers.py +214 -0
  26. pytestflow/backend/report_manager.py +15 -0
  27. pytestflow/backend/sequences_info.py +130 -0
  28. pytestflow/backend/start_backend.py +118 -0
  29. pytestflow/backend/uuids_handler.py +67 -0
  30. pytestflow/backend/websocket_gateway.py +91 -0
  31. pytestflow/cli.py +183 -0
  32. pytestflow/config/__init__.py +0 -0
  33. pytestflow/config/config_manager.py +44 -0
  34. pytestflow/core/README.md +110 -0
  35. pytestflow/core/__init__.py +15 -0
  36. pytestflow/core/context.py +41 -0
  37. pytestflow/core/core.py +112 -0
  38. pytestflow/core/pytestflow_states.py +88 -0
  39. pytestflow/core/runtime_control.py +164 -0
  40. pytestflow/core/seq_file_runner.py +38 -0
  41. pytestflow/core/sequence.py +404 -0
  42. pytestflow/core/utils.py +81 -0
  43. pytestflow/flow_utils/README.md +6 -0
  44. pytestflow/flow_utils/__init__.py +0 -0
  45. pytestflow/flow_utils/conditions.py +0 -0
  46. pytestflow/flow_utils/transitions.py +0 -0
  47. pytestflow/starter_here.md +43 -0
  48. pytestflow/steps/README.md +43 -0
  49. pytestflow/steps/__init__.py +15 -0
  50. pytestflow/steps/action_step.py +94 -0
  51. pytestflow/steps/common.py +51 -0
  52. pytestflow/steps/df_numeric_limits.py +151 -0
  53. pytestflow/steps/flow_control.py +86 -0
  54. pytestflow/steps/message_pop_up.py +76 -0
  55. pytestflow/steps/numeric_limit.py +109 -0
  56. pytestflow/steps/pass_fail.py +49 -0
  57. pytestflow/steps/string_check.py +104 -0
  58. pytestflow/steps/waveform_limit.py +170 -0
  59. pytestflow-0.2.0.dist-info/METADATA +73 -0
  60. pytestflow-0.2.0.dist-info/RECORD +63 -0
  61. pytestflow-0.2.0.dist-info/WHEEL +5 -0
  62. pytestflow-0.2.0.dist-info/entry_points.txt +2 -0
  63. pytestflow-0.2.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,94 @@
1
+ # pytestflow/steps/action_step.py
2
+
3
+ from prefect.artifacts import create_markdown_artifact
4
+ from pytestflow.core.core import StepWrapper
5
+ from pytestflow.core.pytestflow_states import PyTestflowDone, PyTestflowError
6
+ from pytestflow.core.utils import get_data_for_gui
7
+ from pytestflow.steps.common import get_metadata_from_prefect_context
8
+ from datetime import datetime
9
+ import re
10
+
11
+
12
+ class ActionStep(StepWrapper):
13
+ def __init__(self, fn, *, name=None, store_as=None, autowire=True, **task_kwargs):
14
+ super().__init__(fn, name=name, autowire=autowire, **task_kwargs)
15
+ self.store_as = store_as
16
+ self.step_type = "action"
17
+
18
+
19
+ def _run(self, *args, **kwargs):
20
+ from pytestflow.core.context import ptf_context
21
+
22
+ try:
23
+ # Execute user code (inside the Prefect task created by StepWrapper)
24
+ result = super()._run(*args, **kwargs)
25
+
26
+ # Optionally store the result in the context
27
+ if self.store_as:
28
+ ptf_context.locals[self.store_as] = result
29
+
30
+ result_data = {
31
+ "step_status": "done",
32
+ "output": result,
33
+ "step_type": self.step_type,
34
+ "pytestflow_timestamp": datetime.utcnow().isoformat(),
35
+ "store_as": self.store_as,
36
+ }
37
+
38
+ result_data.update(self.get_meta_info())
39
+ result_data.update(get_metadata_from_prefect_context())
40
+
41
+ # Send End data to GUI
42
+ get_data_for_gui(self, result_data.get("end_time"), result_data)
43
+
44
+ # Prefect UI artifact (best-effort)
45
+ try:
46
+ safe_step_name = re.sub(r"[^a-z0-9\\-]", "-", self.name.lower())
47
+ artifact_md = (
48
+ f"### Step: `{self.name}`\n"
49
+ f"- **Output:** `{result}`\n"
50
+ f"- **Stored as:** `{self.store_as or 'None'}`\n"
51
+ f"- **Status:** ✅ DONE"
52
+ )
53
+ create_markdown_artifact(
54
+ key=f"result-{safe_step_name}",
55
+ markdown=artifact_md,
56
+ description=f"Action result for `{self.name}`",
57
+ )
58
+ except Exception:
59
+ pass
60
+
61
+ return PyTestflowDone(
62
+ ptf_result=result_data,
63
+ result=result,
64
+ message="Action step completed",
65
+ )
66
+
67
+ except Exception as e:
68
+ result_data = {
69
+ "step_status": "error",
70
+ "error": str(e),
71
+ "step_type": "action",
72
+ "pytestflow_timestamp": datetime.utcnow().isoformat(),
73
+ "store_as": self.store_as,
74
+ }
75
+
76
+ result_data.update(self.get_meta_info())
77
+ # optional: include Prefect metadata even on error (useful)
78
+ result_data.update(get_metadata_from_prefect_context())
79
+
80
+ return PyTestflowError(
81
+ ptf_result=result_data,
82
+ message=f"Action step `{self.name}` failed",
83
+ result=None,
84
+ )
85
+
86
+
87
+ def action_step(*, name=None, store_as=None, autowire=True, **task_kwargs):
88
+ """
89
+ Decorator factory for action steps.
90
+ """
91
+ def decorator(fn):
92
+ # IMPORTANT: already task-wrapped by StepWrapper
93
+ return ActionStep(fn, name=name, store_as=store_as, autowire=autowire, **task_kwargs)
94
+ return decorator
@@ -0,0 +1,51 @@
1
+ from prefect.context import get_run_context
2
+ import warnings
3
+
4
+ def safe_getattr(obj, attr, default=None):
5
+ try:
6
+ return getattr(obj, attr, default)
7
+ except Exception:
8
+ return default
9
+
10
+
11
+ def get_metadata_from_prefect_context():
12
+ metadata = {
13
+ "flow_run_id": None,
14
+ "flow_name": None,
15
+ "task_run_id": None,
16
+ "task_name": None,
17
+ }
18
+
19
+ # 1) Get task metadata from run context (works well inside tasks)
20
+ try:
21
+ run_ctx = get_run_context()
22
+ task_run = safe_getattr(run_ctx, "task_run", None)
23
+
24
+ metadata["task_run_id"] = safe_getattr(task_run, "id")
25
+ metadata["task_name"] = safe_getattr(task_run, "name")
26
+ except RuntimeError:
27
+ warnings.warn(
28
+ "Prefect context is unavailable. Metadata enrichment is skipped.",
29
+ RuntimeWarning,
30
+ )
31
+ return metadata
32
+
33
+ # 2) Get flow metadata from Prefect runtime (most reliable)
34
+ try:
35
+ from prefect.runtime import flow_run
36
+ metadata["flow_run_id"] = safe_getattr(flow_run, "id")
37
+ metadata["flow_name"] = safe_getattr(flow_run, "name")
38
+ except Exception:
39
+ # 3) Fallback: try flow_run from context if present
40
+ try:
41
+ run_ctx = get_run_context()
42
+ flow_run_obj = safe_getattr(run_ctx, "flow_run", None)
43
+ metadata["flow_run_id"] = metadata["flow_run_id"] or safe_getattr(flow_run_obj, "id")
44
+ metadata["flow_name"] = metadata["flow_name"] or safe_getattr(flow_run_obj, "name")
45
+ except Exception:
46
+ pass
47
+
48
+ return metadata
49
+
50
+ def get_runtime_value(value):
51
+ return value() if callable(value) else value
@@ -0,0 +1,151 @@
1
+ from prefect.artifacts import create_markdown_artifact
2
+ from pytestflow.core.core import StepWrapper
3
+ from pytestflow.core.pytestflow_states import PyTestflowPassed, PyTestflowFailed
4
+ from pytestflow.core.utils import get_data_for_gui
5
+ from pytestflow.steps.common import get_metadata_from_prefect_context, get_runtime_value
6
+ from typing import Callable
7
+ import pandas as pd
8
+ import re
9
+
10
+
11
+ class DFNumericLimitsStep(StepWrapper):
12
+ def __init__(self, fn, *, limits_df: pd.DataFrame, name=None, autowire=True, **task_kwargs):
13
+ super().__init__(fn, name=name, autowire=autowire, **task_kwargs)
14
+ self.limits_df = limits_df
15
+ self.step_type = "df_numeric_limits"
16
+
17
+
18
+ def _run(self, *args, **kwargs):
19
+ values = super()._run(*args, **kwargs)
20
+
21
+ limits_df = get_runtime_value(self.limits_df)
22
+
23
+ if not isinstance(values, (dict, pd.Series)):
24
+ raise TypeError("Return value must be dict or pd.Series")
25
+
26
+ results = []
27
+ overall_status = "passed"
28
+
29
+ for _, row in limits_df.iterrows():
30
+ varname = row["name"]
31
+
32
+ try:
33
+ val = values[varname]
34
+ limit = row["limit"]
35
+ mode = row["mode"]
36
+
37
+ if mode == "ge":
38
+ status = "passed" if val >= limit else "failed"
39
+
40
+ elif mode == "le":
41
+ status = "passed" if val <= limit else "failed"
42
+
43
+ elif mode == "between":
44
+ low, high = limit
45
+ status = "passed" if low <= val <= high else "failed"
46
+
47
+ elif mode == "outside":
48
+ low, high = limit
49
+ status = "passed" if val < low or val > high else "failed"
50
+
51
+ else:
52
+ raise ValueError(f"Unsupported mode: {mode}")
53
+
54
+ if status != "passed":
55
+ overall_status = "failed"
56
+
57
+ result = {
58
+ "name": varname,
59
+ "value": val,
60
+ "limit": limit,
61
+ "mode": mode,
62
+ "step_status": status,
63
+ }
64
+
65
+ except Exception as e:
66
+ result = {
67
+ "name": varname,
68
+ "step_status": "error",
69
+ "error": str(e),
70
+ }
71
+ overall_status = "failed"
72
+
73
+ results.append(result)
74
+
75
+ result_data = {
76
+ "step_status": overall_status,
77
+ "output": values,
78
+ "per_variable": results,
79
+ "step_type": self.step_type,
80
+ }
81
+
82
+ result_data.update(self.get_meta_info())
83
+ result_data.update(get_metadata_from_prefect_context())
84
+
85
+ # Send End data to GUI
86
+ get_data_for_gui(self, result_data.get("end_time"), result_data)
87
+
88
+ # Prefect UI artifact (best-effort)
89
+ try:
90
+ safe_step_name = re.sub(r"[^a-z0-9\-]", "-", self.name.lower())
91
+
92
+ md_lines = [
93
+ f"### Step: `{self.name}`",
94
+ f"- **Status:** {'✅ PASSED' if overall_status == 'passed' else '❌ FAILED'}",
95
+ "",
96
+ ]
97
+
98
+ if overall_status == "failed":
99
+ failed_results = [r for r in results if r.get("step_status") != "passed"]
100
+
101
+ md_lines += [
102
+ f"**{len(failed_results)}** variable(s) failed limits.",
103
+ "",
104
+ "| Variable | Value | Limit | Mode | Status |",
105
+ "|---|---|---|---|---|",
106
+ ]
107
+
108
+ for r in failed_results[:10]:
109
+ md_lines.append(
110
+ f"| {r.get('name')} | {r.get('value', 'NA')} | {r.get('limit', 'NA')} | "
111
+ f"{r.get('mode', 'NA')} | {r.get('step_status', 'error')} |"
112
+ )
113
+
114
+ if len(failed_results) > 10:
115
+ md_lines.append(
116
+ f"\n… and **{len(failed_results) - 10} more**. See full report for details."
117
+ )
118
+
119
+ create_markdown_artifact(
120
+ key=f"result-{safe_step_name}",
121
+ markdown="\n".join(md_lines),
122
+ description=f"DataFrame numeric limits for `{self.name}`",
123
+ )
124
+
125
+ except Exception:
126
+ pass
127
+
128
+ return (
129
+ PyTestflowPassed(ptf_result=result_data)
130
+ if overall_status == "passed"
131
+ else PyTestflowFailed(
132
+ ptf_result=result_data,
133
+ message=f"{self.name} failed"
134
+ )
135
+ )
136
+
137
+
138
+ def df_numeric_limits_step(*, limits_df: pd.DataFrame, name=None, autowire=True, **task_kwargs):
139
+ """
140
+ Decorator factory for DataFrame numeric limit steps.
141
+ """
142
+ def decorator(fn: Callable):
143
+ # IMPORTANT: already task-wrapped by StepWrapper
144
+ return DFNumericLimitsStep(
145
+ fn,
146
+ limits_df=limits_df,
147
+ name=name,
148
+ autowire=autowire,
149
+ **task_kwargs,
150
+ )
151
+ return decorator
@@ -0,0 +1,86 @@
1
+ from datetime import datetime
2
+ from typing import Callable
3
+
4
+ from pytestflow.core.context import ptf_context
5
+ from pytestflow.core.core import StepWrapper
6
+ from pytestflow.core.pytestflow_states import PyTestflowDone, PyTestflowError
7
+ from pytestflow.core.utils import get_data_for_gui
8
+ from pytestflow.steps.common import get_metadata_from_prefect_context, get_runtime_value
9
+
10
+
11
+ class FlowControlStep(StepWrapper):
12
+ def __init__(self, fn, *, next_steps={0:"next", 1:"next"}, name=None, store_as=None, autowire=True, **task_kwargs):
13
+ super().__init__(fn, name=name, autowire=autowire, **task_kwargs)
14
+ self.next_steps = next_steps
15
+ self.store_as = store_as
16
+ self.step_type = "flow_control"
17
+
18
+ def _run(self, *args, **kwargs):
19
+ try:
20
+ result = super()._run(*args, **kwargs)
21
+
22
+ next_steps_map = get_runtime_value(self.next_steps)
23
+ store_as = get_runtime_value(self.store_as)
24
+
25
+ if store_as:
26
+ ptf_context.locals[store_as] = result
27
+
28
+ actual_next_steps = next_steps_map.get(result, "next")
29
+ ptf_context.locals["_ptf_next_step"] = actual_next_steps
30
+
31
+ result_data = {
32
+ "step_status": "done",
33
+ "output": result,
34
+ "step_type": self.step_type,
35
+ "pytestflow_timestamp": datetime.utcnow().isoformat(),
36
+ "store_as": store_as,
37
+ "flow_control_next_steps": next_steps_map,
38
+ "selected_next_step": actual_next_steps,
39
+ "retry_n": ptf_context.locals.get("retry_n"),
40
+ }
41
+
42
+ result_data.update(self.get_meta_info())
43
+ result_data.update(get_metadata_from_prefect_context())
44
+
45
+ get_data_for_gui(self, result_data.get("end_time"), result_data)
46
+
47
+ return PyTestflowDone(
48
+ ptf_result=result_data,
49
+ message="Flow control step completed",
50
+ )
51
+
52
+ except Exception as exc:
53
+ result_data = {
54
+ "step_status": "error",
55
+ "error": str(exc),
56
+ "step_type": self.step_type,
57
+ "pytestflow_timestamp": datetime.utcnow().isoformat(),
58
+ "store_as": get_runtime_value(self.store_as),
59
+ "retry_n": ptf_context.locals.get("retry_n"),
60
+ }
61
+
62
+ result_data.update(self.get_meta_info())
63
+ result_data.update(get_metadata_from_prefect_context())
64
+
65
+ return PyTestflowError(
66
+ ptf_result=result_data,
67
+ message=f"Flow control step `{self.name}` failed",
68
+ )
69
+
70
+
71
+ def flow_control_step(*, next_steps={0:"next", 1:"next"}, name=None, store_as=None, autowire=True, **task_kwargs):
72
+ """
73
+ Decorator factory for control-only steps that steer sequence execution.
74
+ """
75
+
76
+ def decorator(fn: Callable):
77
+ return FlowControlStep(
78
+ fn,
79
+ name=name,
80
+ next_steps=next_steps,
81
+ store_as=store_as,
82
+ autowire=autowire,
83
+ **task_kwargs,
84
+ )
85
+
86
+ return decorator
@@ -0,0 +1,76 @@
1
+ from pytestflow.core.core import StepWrapper
2
+ from pytestflow.core.pytestflow_states import PyTestflowDone
3
+ from pytestflow.core.utils import get_data_for_gui
4
+ from pytestflow.steps.common import get_metadata_from_prefect_context, get_runtime_value
5
+ from typing import Callable
6
+ from pytestflow.core.runtime_control import runtime_control
7
+
8
+
9
+ class MessagePopUpStep(StepWrapper):
10
+ def __init__(self, fn, *, show_response_box=False, title="", msg="", buttons=[], name=None, store_as=None, autowire=True, **task_kwargs):
11
+ super().__init__(fn, name=name, autowire=autowire, **task_kwargs)
12
+ self.step_type = "message_pop_up"
13
+ self.show_response_box = show_response_box
14
+ self.title = title
15
+ self.msg = msg
16
+ self.buttons = buttons
17
+ self.store_as = store_as
18
+
19
+ def _run(self, *args, **kwargs):
20
+ # Executes inside the Prefect task created by StepWrapper
21
+ value = super()._run(*args, **kwargs)
22
+
23
+ show_response_box = get_runtime_value(self.show_response_box)
24
+ title = get_runtime_value(self.title)
25
+ msg = get_runtime_value(self.msg)
26
+ buttons = get_runtime_value(self.buttons)
27
+ store_as = get_runtime_value(self.store_as)
28
+
29
+ popup_data = {
30
+ "isResponseBoxVisible": show_response_box,
31
+ "title": title,
32
+ "msg": msg,
33
+ "buttons": buttons,
34
+ }
35
+
36
+ user_response = runtime_control.show_popup(popup_data)
37
+
38
+ if store_as:
39
+ from pytestflow.core.context import ptf_context
40
+ ptf_context.locals[store_as] = user_response
41
+
42
+ result_data = {
43
+ "step_status": "done",
44
+ "step_type": self.step_type,
45
+ "user_response": user_response,
46
+ "show_response_box": show_response_box,
47
+ "title": title,
48
+ "msg": msg,
49
+ "buttons": buttons,
50
+ "store_as": store_as,
51
+ }
52
+
53
+ result_data.update(self.get_meta_info())
54
+ result_data.update(get_metadata_from_prefect_context())
55
+
56
+ # Send End data to GUI
57
+ get_data_for_gui(self, result_data.get("end_time"), result_data)
58
+
59
+ return PyTestflowDone(ptf_result=result_data)
60
+ def message_pop_up_step(*, show_response_box=False, title="TITLE_HERE", msg="MSG_HERE", buttons=["Ok"], name=None, store_as=None, autowire=True, **task_kwargs):
61
+ """
62
+ Decorator factory for message pop-up steps.
63
+ """
64
+ def decorator(fn: Callable):
65
+ # IMPORTANT: already task-wrapped by StepWrapper
66
+ return MessagePopUpStep(
67
+ fn,
68
+ show_response_box=show_response_box,
69
+ title=title,
70
+ msg=msg,
71
+ buttons=buttons,
72
+ name=name,
73
+ store_as=store_as,
74
+ autowire=autowire,
75
+ **task_kwargs)
76
+ return decorator
@@ -0,0 +1,109 @@
1
+ # numeric_limit_step.py
2
+ from pytestflow.core.core import StepWrapper
3
+ from pytestflow.core.pytestflow_states import PyTestflowPassed, PyTestflowFailed
4
+ from pytestflow.core.utils import get_data_for_gui
5
+ from pytestflow.steps.common import get_metadata_from_prefect_context, get_runtime_value
6
+ from typing import Callable
7
+ import re
8
+ from prefect.artifacts import create_markdown_artifact
9
+
10
+
11
+ class NumericLimitStep(StepWrapper):
12
+ def __init__(self, fn, *, limit, mode, name=None, store_as=None, autowire=True, **task_kwargs):
13
+ super().__init__(fn, name=name, autowire=autowire, **task_kwargs)
14
+ self.limit = limit
15
+ self.mode = mode
16
+ self.store_as = store_as
17
+ self.step_type = "numeric_limit"
18
+
19
+
20
+ def _run(self, *args, **kwargs):
21
+ from pytestflow.core.context import ptf_context
22
+
23
+ value = super()._run(*args, **kwargs)
24
+
25
+ limit = get_runtime_value(self.limit)
26
+ mode = get_runtime_value(self.mode)
27
+ store_as = get_runtime_value(self.store_as)
28
+
29
+ if not isinstance(value, (int, float)):
30
+ raise TypeError("Return value must be numeric")
31
+
32
+ if mode == "ge":
33
+ passed = value >= limit
34
+
35
+ elif mode == "le":
36
+ passed = value <= limit
37
+
38
+ elif mode == "between":
39
+ assert isinstance(limit, (list, tuple)) and len(limit) == 2
40
+ passed = limit[0] <= value <= limit[1]
41
+
42
+ elif mode == "outside":
43
+ assert isinstance(limit, (list, tuple)) and len(limit) == 2
44
+ passed = value < limit[0] or value > limit[1]
45
+
46
+ else:
47
+ raise ValueError(f"Unsupported mode: {mode}")
48
+
49
+ if store_as:
50
+ ptf_context.locals[store_as] = value
51
+
52
+ result_data = {
53
+ "measurement": value,
54
+ "limit": limit,
55
+ "mode": mode,
56
+ "store_as": store_as,
57
+ "step_status": "passed" if passed else "failed",
58
+ "output": value,
59
+ "step_type": self.step_type,
60
+ }
61
+
62
+ result_data.update(self.get_meta_info())
63
+ result_data.update(get_metadata_from_prefect_context())
64
+
65
+ # Send End data to GUI
66
+ get_data_for_gui(self, result_data.get("end_time"), result_data)
67
+
68
+ try:
69
+ safe_step_name = re.sub(r"[^a-z0-9\-]", "-", self.name.lower())
70
+
71
+ artifact_md = (
72
+ f"### Step: `{self.name}`\n"
73
+ f"- **Measurement:** `{value}`\n"
74
+ f"- **Limit ({mode}):** `{limit}`\n"
75
+ f"- **Status:** {'✅ PASSED' if passed else '❌ FAILED'}"
76
+ )
77
+
78
+ create_markdown_artifact(
79
+ key=f"result-{safe_step_name}",
80
+ markdown=artifact_md,
81
+ description=f"Numeric limit check for `{self.name}`",
82
+ )
83
+
84
+ except RuntimeError:
85
+ pass
86
+
87
+ return (
88
+ PyTestflowPassed(ptf_result=result_data)
89
+ if passed
90
+ else PyTestflowFailed(
91
+ ptf_result=result_data,
92
+ message=f"{self.name} failed"
93
+ )
94
+ )
95
+
96
+
97
+ def numeric_limit_step(*, limit, mode="ge", name=None, store_as=None, autowire=True, **task_kwargs):
98
+ def decorator(fn: Callable):
99
+ # IMPORTANT: return the step instance (already task-wrapped by StepWrapper)
100
+ return NumericLimitStep(
101
+ fn,
102
+ limit=limit,
103
+ mode=mode,
104
+ name=name,
105
+ store_as=store_as,
106
+ autowire=autowire,
107
+ **task_kwargs,
108
+ )
109
+ return decorator
@@ -0,0 +1,49 @@
1
+ from pytestflow.core.core import StepWrapper
2
+ from pytestflow.core.pytestflow_states import PyTestflowPassed, PyTestflowFailed
3
+ from pytestflow.core.utils import get_data_for_gui
4
+ from pytestflow.steps.common import get_metadata_from_prefect_context
5
+ from typing import Callable
6
+
7
+
8
+ class PassFailStep(StepWrapper):
9
+ def __init__(self, fn, *, name=None, autowire=True, **task_kwargs):
10
+ super().__init__(fn, name=name, autowire=autowire, **task_kwargs)
11
+ self.step_type = "pass_fail"
12
+
13
+
14
+ def _run(self, *args, **kwargs):
15
+ # Executes inside the Prefect task created by StepWrapper
16
+ value = super()._run(*args, **kwargs)
17
+
18
+ passed = bool(value)
19
+
20
+ result_data = {
21
+ "step_status": "passed" if passed else "failed",
22
+ "output": value,
23
+ "step_type": self.step_type,
24
+ }
25
+
26
+ result_data.update(self.get_meta_info())
27
+ result_data.update(get_metadata_from_prefect_context())
28
+
29
+ # Send End data to GUI
30
+ get_data_for_gui(self, result_data.get("end_time"), result_data)
31
+
32
+ return (
33
+ PyTestflowPassed(ptf_result=result_data)
34
+ if passed
35
+ else PyTestflowFailed(
36
+ ptf_result=result_data,
37
+ message=f"{self.name} failed"
38
+ )
39
+ )
40
+
41
+
42
+ def pass_fail_step(*, name=None, autowire=True, **task_kwargs):
43
+ """
44
+ Decorator factory for pass/fail steps.
45
+ """
46
+ def decorator(fn: Callable):
47
+ # IMPORTANT: already task-wrapped by StepWrapper
48
+ return PassFailStep(fn, name=name, autowire=autowire, **task_kwargs)
49
+ return decorator