batautomate 0.1.0__tar.gz

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.
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: batautomate
3
+ Version: 0.1.0
4
+ Summary: Core execution engine and standard action libraries for BAT Automate RPA platform
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: pydantic>=2.0.0
8
+ Requires-Dist: playwright>=1.40.0
9
+ Requires-Dist: pandas>=2.0.0
10
+ Requires-Dist: openpyxl>=3.1.0
11
+ Requires-Dist: requests>=2.28.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
14
+
15
+ # bat-core
16
+
17
+ Execution Runtime Engine and Standard Action Libraries for BAT Automate RPA Platform.
18
+
19
+ ## Directory Structure
20
+
21
+ ```text
22
+ bat-core/
23
+ ├── pyproject.toml # Project configuration & dependencies
24
+ ├── requirements.txt # Package dependencies list
25
+ ├── README.md # Documentation
26
+ ├── bat_core/ # Core Python Package
27
+ │ ├── __init__.py
28
+ │ ├── cli.py # CLI Runner
29
+ │ ├── models/ # Flow & Execution Models (Pydantic)
30
+ │ │ ├── __init__.py
31
+ │ │ ├── flow.py
32
+ │ │ └── context.py
33
+ │ ├── engine/ # Flow Interpreter & Evaluator Engine
34
+ │ │ ├── __init__.py
35
+ │ │ ├── interpreter.py
36
+ │ │ ├── evaluator.py
37
+ │ │ └── logger.py
38
+ │ └── actions/ # Action Libraries & Registry
39
+ │ ├── __init__.py
40
+ │ ├── base.py
41
+ │ ├── registry.py
42
+ │ ├── logic.py
43
+ │ ├── data_excel.py
44
+ │ ├── web_playwright.py
45
+ │ └── http_api.py
46
+ ├── examples/ # Sample Flow JSON files
47
+ │ └── sample_flow.json
48
+ └── tests/ # Unit Tests
49
+ └── test_engine.py
50
+ ```
51
+
52
+ ---
53
+
54
+ ## คู่มือการใช้งานและการสั่งรันคำสั่ง (CLI Execution Guide)
55
+
56
+ โมดูล `bat-core` สามารถสั่งรันกระบวนการอัตโนมัติ (RPA Flow) ผ่าน Command Line Interface (CLI) ได้ดังนี้:
57
+
58
+ ### 1. การติดตั้งแบบ Editable (สำหรับการพัฒนา)
59
+
60
+ ติดตั้ง Package และ Dependencies ในสภาพแวดล้อม Python:
61
+ ```powershell
62
+ pip install -e .
63
+ ```
64
+
65
+ ### 2. รูปแบบคำสั่งการรันพื้นฐาน
66
+
67
+ รันผ่านสคริปต์ Python Module:
68
+ ```powershell
69
+ python -m bat_core.cli run examples/sample_flow.json
70
+ ```
71
+
72
+ หรือรันผ่านคำสั่ง CLI สั้น (เมื่อทำการติดตั้ง Package แล้ว):
73
+ ```powershell
74
+ bat-core run examples/sample_flow.json
75
+ ```
76
+
77
+ ### 3. ตัวเลือกและพารามิเตอร์เสริม (CLI Options)
78
+
79
+ - **`--vars` (Override / Inject Variables):** ส่งค่าตัวแปรจากภายนอกเข้าไปใน Flow รูปแบบ JSON String
80
+ ```powershell
81
+ python -m bat_core.cli run examples/sample_flow.json --vars "{\"target_url\": \"https://httpbin.org/get\"}"
82
+ ```
83
+
84
+ - **`--log-dir` (Save Execution Logs):** กำหนดไดเรกทอรีสำหรับบันทึกไฟล์ JSON Execution Log
85
+ ```powershell
86
+ python -m bat_core.cli run examples/sample_flow.json --log-dir "./logs"
87
+ ```
88
+
89
+ ### 4. การรันชุดทดสอบ (Unit Testing)
90
+
91
+ ทดสอบความถูกต้องของระบบและ Action ทั้งหมด:
92
+ ```powershell
93
+ python -m pytest tests/
94
+ ```
95
+
@@ -0,0 +1,81 @@
1
+ # bat-core
2
+
3
+ Execution Runtime Engine and Standard Action Libraries for BAT Automate RPA Platform.
4
+
5
+ ## Directory Structure
6
+
7
+ ```text
8
+ bat-core/
9
+ ├── pyproject.toml # Project configuration & dependencies
10
+ ├── requirements.txt # Package dependencies list
11
+ ├── README.md # Documentation
12
+ ├── bat_core/ # Core Python Package
13
+ │ ├── __init__.py
14
+ │ ├── cli.py # CLI Runner
15
+ │ ├── models/ # Flow & Execution Models (Pydantic)
16
+ │ │ ├── __init__.py
17
+ │ │ ├── flow.py
18
+ │ │ └── context.py
19
+ │ ├── engine/ # Flow Interpreter & Evaluator Engine
20
+ │ │ ├── __init__.py
21
+ │ │ ├── interpreter.py
22
+ │ │ ├── evaluator.py
23
+ │ │ └── logger.py
24
+ │ └── actions/ # Action Libraries & Registry
25
+ │ ├── __init__.py
26
+ │ ├── base.py
27
+ │ ├── registry.py
28
+ │ ├── logic.py
29
+ │ ├── data_excel.py
30
+ │ ├── web_playwright.py
31
+ │ └── http_api.py
32
+ ├── examples/ # Sample Flow JSON files
33
+ │ └── sample_flow.json
34
+ └── tests/ # Unit Tests
35
+ └── test_engine.py
36
+ ```
37
+
38
+ ---
39
+
40
+ ## คู่มือการใช้งานและการสั่งรันคำสั่ง (CLI Execution Guide)
41
+
42
+ โมดูล `bat-core` สามารถสั่งรันกระบวนการอัตโนมัติ (RPA Flow) ผ่าน Command Line Interface (CLI) ได้ดังนี้:
43
+
44
+ ### 1. การติดตั้งแบบ Editable (สำหรับการพัฒนา)
45
+
46
+ ติดตั้ง Package และ Dependencies ในสภาพแวดล้อม Python:
47
+ ```powershell
48
+ pip install -e .
49
+ ```
50
+
51
+ ### 2. รูปแบบคำสั่งการรันพื้นฐาน
52
+
53
+ รันผ่านสคริปต์ Python Module:
54
+ ```powershell
55
+ python -m bat_core.cli run examples/sample_flow.json
56
+ ```
57
+
58
+ หรือรันผ่านคำสั่ง CLI สั้น (เมื่อทำการติดตั้ง Package แล้ว):
59
+ ```powershell
60
+ bat-core run examples/sample_flow.json
61
+ ```
62
+
63
+ ### 3. ตัวเลือกและพารามิเตอร์เสริม (CLI Options)
64
+
65
+ - **`--vars` (Override / Inject Variables):** ส่งค่าตัวแปรจากภายนอกเข้าไปใน Flow รูปแบบ JSON String
66
+ ```powershell
67
+ python -m bat_core.cli run examples/sample_flow.json --vars "{\"target_url\": \"https://httpbin.org/get\"}"
68
+ ```
69
+
70
+ - **`--log-dir` (Save Execution Logs):** กำหนดไดเรกทอรีสำหรับบันทึกไฟล์ JSON Execution Log
71
+ ```powershell
72
+ python -m bat_core.cli run examples/sample_flow.json --log-dir "./logs"
73
+ ```
74
+
75
+ ### 4. การรันชุดทดสอบ (Unit Testing)
76
+
77
+ ทดสอบความถูกต้องของระบบและ Action ทั้งหมด:
78
+ ```powershell
79
+ python -m pytest tests/
80
+ ```
81
+
@@ -0,0 +1,5 @@
1
+ """
2
+ bat-core: Core Execution Engine for BAT Automate RPA Platform
3
+ """
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,17 @@
1
+ """
2
+ Actions registry and standard action implementations.
3
+ """
4
+
5
+ from .base import BaseAction
6
+ from .registry import ActionRegistry, register_action
7
+ from . import logic, data_excel, web_playwright, http_api
8
+
9
+ __all__ = [
10
+ "BaseAction",
11
+ "ActionRegistry",
12
+ "register_action",
13
+ "logic",
14
+ "data_excel",
15
+ "web_playwright",
16
+ "http_api",
17
+ ]
@@ -0,0 +1,20 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict
3
+
4
+ from ..models.context import ExecutionContext
5
+
6
+
7
+ class BaseAction(ABC):
8
+ """
9
+ Abstract base class for all BAT Automate Actions.
10
+ """
11
+
12
+ action_type: str = "base"
13
+
14
+ @abstractmethod
15
+ def execute(self, parameters: Dict[str, Any], context: ExecutionContext) -> Any:
16
+ """
17
+ Executes the action with given parameters and context.
18
+ Returns output data to be stored or used by downstream steps.
19
+ """
20
+ pass
@@ -0,0 +1,39 @@
1
+ from pathlib import Path
2
+ from typing import Any, Dict
3
+ import pandas as pd
4
+
5
+ from .base import BaseAction
6
+ from .registry import register_action
7
+ from ..models.context import ExecutionContext
8
+
9
+
10
+ @register_action("excel.read")
11
+ class ExcelReadAction(BaseAction):
12
+ def execute(self, parameters: Dict[str, Any], context: ExecutionContext) -> Any:
13
+ file_path = parameters.get("file_path")
14
+ sheet_name = parameters.get("sheet_name", 0)
15
+
16
+ if not file_path or not Path(file_path).exists():
17
+ raise FileNotFoundError(f"Excel file not found: {file_path}")
18
+
19
+ df = pd.read_excel(file_path, sheet_name=sheet_name)
20
+ records = df.to_dict(orient="records")
21
+ return records
22
+
23
+
24
+ @register_action("excel.write")
25
+ class ExcelWriteAction(BaseAction):
26
+ def execute(self, parameters: Dict[str, Any], context: ExecutionContext) -> Any:
27
+ file_path = parameters.get("file_path")
28
+ data = parameters.get("data", [])
29
+ sheet_name = parameters.get("sheet_name", "Sheet1")
30
+
31
+ if isinstance(data, list):
32
+ df = pd.DataFrame(data)
33
+ elif isinstance(data, dict):
34
+ df = pd.DataFrame([data])
35
+ else:
36
+ raise ValueError("Data for excel.write must be a list of dicts or a dict.")
37
+
38
+ df.to_excel(file_path, sheet_name=sheet_name, index=False)
39
+ return {"rows_written": len(df), "file_path": file_path}
@@ -0,0 +1,36 @@
1
+ from typing import Any, Dict
2
+ import requests
3
+
4
+ from .base import BaseAction
5
+ from .registry import register_action
6
+ from ..models.context import ExecutionContext
7
+
8
+
9
+ @register_action("http.request")
10
+ class HttpRequestAction(BaseAction):
11
+ def execute(self, parameters: Dict[str, Any], context: ExecutionContext) -> Any:
12
+ url = parameters.get("url")
13
+ method = parameters.get("method", "GET").upper()
14
+ headers = parameters.get("headers", {})
15
+ payload = parameters.get("payload")
16
+ timeout = parameters.get("timeout", 30)
17
+
18
+ response = requests.request(
19
+ method=method,
20
+ url=url,
21
+ headers=headers,
22
+ json=payload if isinstance(payload, dict) else None,
23
+ data=payload if isinstance(payload, str) else None,
24
+ timeout=timeout
25
+ )
26
+
27
+ try:
28
+ body = response.json()
29
+ except Exception:
30
+ body = response.text
31
+
32
+ return {
33
+ "status_code": response.status_code,
34
+ "headers": dict(response.headers),
35
+ "body": body
36
+ }
@@ -0,0 +1,24 @@
1
+ import time
2
+ from typing import Any, Dict
3
+
4
+ from .base import BaseAction
5
+ from .registry import register_action
6
+ from ..models.context import ExecutionContext
7
+
8
+
9
+ @register_action("logic.set_variable")
10
+ class SetVariableAction(BaseAction):
11
+ def execute(self, parameters: Dict[str, Any], context: ExecutionContext) -> Any:
12
+ name = parameters.get("name")
13
+ value = parameters.get("value")
14
+ if name:
15
+ context.set_variable(name, value)
16
+ return value
17
+
18
+
19
+ @register_action("logic.delay")
20
+ class DelayAction(BaseAction):
21
+ def execute(self, parameters: Dict[str, Any], context: ExecutionContext) -> Any:
22
+ seconds = float(parameters.get("seconds", 1.0))
23
+ time.sleep(seconds)
24
+ return {"delayed_seconds": seconds}
@@ -0,0 +1,32 @@
1
+ from typing import Dict, Type
2
+ from .base import BaseAction
3
+
4
+
5
+ class ActionRegistry:
6
+ """
7
+ Registry mapping action string names (e.g. 'excel.read', 'logic.set_variable') to Action Classes.
8
+ """
9
+
10
+ _registry: Dict[str, Type[BaseAction]] = {}
11
+
12
+ @classmethod
13
+ def register(cls, action_type: str, action_class: Type[BaseAction]) -> None:
14
+ cls._registry[action_type] = action_class
15
+
16
+ @classmethod
17
+ def get(cls, action_type: str) -> Type[BaseAction]:
18
+ if action_type not in cls._registry:
19
+ raise KeyError(f"Action '{action_type}' is not registered in ActionRegistry.")
20
+ return cls._registry[action_type]
21
+
22
+ @classmethod
23
+ def list_actions(cls) -> Dict[str, Type[BaseAction]]:
24
+ return dict(cls._registry)
25
+
26
+
27
+ def register_action(action_type: str):
28
+ def decorator(cls: Type[BaseAction]):
29
+ cls.action_type = action_type
30
+ ActionRegistry.register(action_type, cls)
31
+ return cls
32
+ return decorator
@@ -0,0 +1,27 @@
1
+ from typing import Any, Dict
2
+ from .base import BaseAction
3
+ from .registry import register_action
4
+ from ..models.context import ExecutionContext
5
+
6
+
7
+ @register_action("web.open")
8
+ class WebOpenAction(BaseAction):
9
+ def execute(self, parameters: Dict[str, Any], context: ExecutionContext) -> Any:
10
+ url = parameters.get("url")
11
+ headless = parameters.get("headless", True)
12
+ return {"action": "web.open", "url": url, "headless": headless, "status": "simulated"}
13
+
14
+
15
+ @register_action("web.click")
16
+ class WebClickAction(BaseAction):
17
+ def execute(self, parameters: Dict[str, Any], context: ExecutionContext) -> Any:
18
+ selector = parameters.get("selector")
19
+ return {"action": "web.click", "selector": selector, "status": "simulated"}
20
+
21
+
22
+ @register_action("web.type")
23
+ class WebTypeAction(BaseAction):
24
+ def execute(self, parameters: Dict[str, Any], context: ExecutionContext) -> Any:
25
+ selector = parameters.get("selector")
26
+ text = parameters.get("text")
27
+ return {"action": "web.type", "selector": selector, "text": text, "status": "simulated"}
@@ -0,0 +1,56 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from .models.flow import FlowDefinition
7
+ from .engine.interpreter import FlowInterpreter
8
+ from .engine.logger import ExecutionLogger
9
+
10
+
11
+ def main():
12
+ parser = argparse.ArgumentParser(description="bat-core CLI Runner")
13
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
14
+
15
+ run_parser = subparsers.add_parser("run", help="Run a Flow JSON file")
16
+ run_parser.add_argument("flow_file", help="Path to flow.json file")
17
+ run_parser.add_argument("--vars", help="Optional JSON string of variables to override", default=None)
18
+ run_parser.add_argument("--log-dir", help="Directory to save execution JSON logs", default=None)
19
+
20
+ args = parser.parse_args()
21
+
22
+ if args.command == "run":
23
+ flow_path = Path(args.flow_file)
24
+ if not flow_path.exists():
25
+ print(f"Error: Flow file '{args.flow_file}' does not exist.", file=sys.stderr)
26
+ sys.exit(1)
27
+
28
+ try:
29
+ content = flow_path.read_text(encoding="utf-8")
30
+ raw_json = json.loads(content)
31
+ flow_def = FlowDefinition.model_validate(raw_json)
32
+ except Exception as e:
33
+ print(f"Error parsing flow JSON: {str(e)}", file=sys.stderr)
34
+ sys.exit(1)
35
+
36
+ extra_vars = {}
37
+ if args.vars:
38
+ try:
39
+ extra_vars = json.loads(args.vars)
40
+ except Exception as e:
41
+ print(f"Error parsing --vars JSON: {str(e)}", file=sys.stderr)
42
+ sys.exit(1)
43
+
44
+ logger = ExecutionLogger(log_dir=args.log_dir)
45
+ interpreter = FlowInterpreter(logger=logger)
46
+ context = interpreter.run_flow(flow_def, initial_vars=extra_vars)
47
+
48
+ if context.has_error:
49
+ sys.exit(1)
50
+ sys.exit(0)
51
+ else:
52
+ parser.print_help()
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()
@@ -0,0 +1,13 @@
1
+ """
2
+ Flow execution engine, evaluator, logger, and interpreter.
3
+ """
4
+
5
+ from .evaluator import VariableEvaluator
6
+ from .logger import ExecutionLogger
7
+ from .interpreter import FlowInterpreter
8
+
9
+ __all__ = [
10
+ "VariableEvaluator",
11
+ "ExecutionLogger",
12
+ "FlowInterpreter",
13
+ ]
@@ -0,0 +1,55 @@
1
+ import os
2
+ import re
3
+ from typing import Any, Dict
4
+
5
+
6
+ class VariableEvaluator:
7
+ """
8
+ Evaluates variable expressions in string formats such as ${var_name} or ${env.ENV_VAR}.
9
+ Also handles dictionary key traversal like ${row.Name}.
10
+ """
11
+
12
+ PATTERN = re.compile(r"\$\{([^}]+)\}")
13
+
14
+ @classmethod
15
+ def evaluate_value(cls, value: Any, context_vars: Dict[str, Any]) -> Any:
16
+ if isinstance(value, str):
17
+ match = cls.PATTERN.fullmatch(value.strip())
18
+ if match:
19
+ var_expr = match.group(1).strip()
20
+ return cls._resolve_expr(var_expr, context_vars)
21
+
22
+ def replace_match(m):
23
+ resolved = cls._resolve_expr(m.group(1).strip(), context_vars)
24
+ return str(resolved) if resolved is not None else ""
25
+
26
+ return cls.PATTERN.sub(replace_match, value)
27
+
28
+ elif isinstance(value, dict):
29
+ return {k: cls.evaluate_value(v, context_vars) for k, v in value.items()}
30
+ elif isinstance(value, list):
31
+ return [cls.evaluate_value(v, context_vars) for v in value]
32
+ else:
33
+ return value
34
+
35
+ @classmethod
36
+ def _resolve_expr(cls, expr: str, context_vars: Dict[str, Any]) -> Any:
37
+ if expr.startswith("env."):
38
+ env_key = expr[4:]
39
+ return os.getenv(env_key, "")
40
+
41
+ parts = expr.split(".")
42
+ root_key = parts[0]
43
+
44
+ if root_key not in context_vars:
45
+ return None
46
+
47
+ current = context_vars[root_key]
48
+ for part in parts[1:]:
49
+ if isinstance(current, dict):
50
+ current = current.get(part)
51
+ elif hasattr(current, part):
52
+ current = getattr(current, part)
53
+ else:
54
+ return None
55
+ return current
@@ -0,0 +1,134 @@
1
+ from datetime import datetime
2
+ from typing import Any, Dict, Optional
3
+
4
+ from .evaluator import VariableEvaluator
5
+ from .logger import ExecutionLogger
6
+ from ..actions.registry import ActionRegistry
7
+ from ..models.context import ExecutionContext, StepResult
8
+ from ..models.flow import FlowDefinition, Step
9
+
10
+
11
+ class FlowInterpreter:
12
+ """
13
+ Interpreter that parses FlowDefinition, evaluates expressions, and executes steps sequentially.
14
+ """
15
+
16
+ def __init__(self, logger: Optional[ExecutionLogger] = None):
17
+ self.logger = logger or ExecutionLogger()
18
+
19
+ def run_flow(self, flow_def: FlowDefinition, initial_vars: Optional[Dict[str, Any]] = None) -> ExecutionContext:
20
+ context = ExecutionContext(flow_name=flow_def.name)
21
+
22
+ if flow_def.variables:
23
+ for k, v in flow_def.variables.items():
24
+ context.set_variable(k, v)
25
+
26
+ if initial_vars:
27
+ for k, v in initial_vars.items():
28
+ context.set_variable(k, v)
29
+
30
+ start_time = datetime.now()
31
+
32
+ try:
33
+ for step in flow_def.steps:
34
+ self._execute_step(step, context)
35
+ context.is_completed = True
36
+ except Exception as e:
37
+ context.has_error = True
38
+ self.logger.logger.error(f"Flow execution failed with unhandled exception: {str(e)}")
39
+
40
+ end_time = datetime.now()
41
+ total_duration = (end_time - start_time).total_seconds()
42
+
43
+ context.metrics.total_steps = len(context.step_results)
44
+ context.metrics.successful_steps = sum(1 for r in context.step_results if r.status == "success")
45
+ context.metrics.failed_steps = sum(1 for r in context.step_results if r.status == "failed")
46
+ context.metrics.skipped_steps = sum(1 for r in context.step_results if r.status == "skipped")
47
+ context.metrics.total_duration_seconds = total_duration
48
+
49
+ self.logger.log_execution_summary(context)
50
+ return context
51
+
52
+ def _execute_step(self, step: Step, context: ExecutionContext) -> None:
53
+ self.logger.log_step_start(step.id, step.name, step.action)
54
+ start_time = datetime.now()
55
+
56
+ evaluated_params = VariableEvaluator.evaluate_value(step.parameters, context.variables)
57
+
58
+ if step.action == "logic.loop":
59
+ self._handle_loop_step(step, evaluated_params, context, start_time)
60
+ return
61
+
62
+ try:
63
+ action_cls = ActionRegistry.get(step.action)
64
+ action_instance = action_cls()
65
+ output = action_instance.execute(evaluated_params, context)
66
+
67
+ if step.output_var and output is not None:
68
+ context.set_variable(step.output_var, output)
69
+
70
+ end_time = datetime.now()
71
+ duration = (end_time - start_time).total_seconds()
72
+
73
+ result = StepResult(
74
+ step_id=step.id,
75
+ step_name=step.name,
76
+ status="success",
77
+ start_time=start_time,
78
+ end_time=end_time,
79
+ duration_seconds=duration,
80
+ output=output
81
+ )
82
+ context.step_results.append(result)
83
+ self.logger.log_step_result(result)
84
+
85
+ except Exception as e:
86
+ end_time = datetime.now()
87
+ duration = (end_time - start_time).total_seconds()
88
+ error_type = "Business" if "Business" in type(e).__name__ else "Technical"
89
+
90
+ result = StepResult(
91
+ step_id=step.id,
92
+ step_name=step.name,
93
+ status="failed",
94
+ start_time=start_time,
95
+ end_time=end_time,
96
+ duration_seconds=duration,
97
+ error_message=str(e),
98
+ error_type=error_type
99
+ )
100
+ context.step_results.append(result)
101
+ self.logger.log_step_result(result)
102
+
103
+ if step.error_handler and step.error_handler.on_error == "continue":
104
+ return
105
+ raise e
106
+
107
+ def _handle_loop_step(self, step: Step, evaluated_params: Dict[str, Any], context: ExecutionContext, start_time: datetime) -> None:
108
+ items = evaluated_params.get("items", [])
109
+ item_var = evaluated_params.get("item_var", "item")
110
+ sub_steps = step.sub_steps or []
111
+
112
+ if not isinstance(items, list):
113
+ items = [items]
114
+
115
+ for index, item in enumerate(items):
116
+ context.set_variable(item_var, item)
117
+ context.set_variable(f"{item_var}_index", index)
118
+ for sub_step in sub_steps:
119
+ self._execute_step(sub_step, context)
120
+
121
+ end_time = datetime.now()
122
+ duration = (end_time - start_time).total_seconds()
123
+
124
+ result = StepResult(
125
+ step_id=step.id,
126
+ step_name=step.name,
127
+ status="success",
128
+ start_time=start_time,
129
+ end_time=end_time,
130
+ duration_seconds=duration,
131
+ output={"total_items_processed": len(items)}
132
+ )
133
+ context.step_results.append(result)
134
+ self.logger.log_step_result(result)
@@ -0,0 +1,55 @@
1
+ import json
2
+ import logging
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from ..models.context import ExecutionContext, StepResult
8
+
9
+
10
+ class ExecutionLogger:
11
+ """
12
+ Structured logger for recording flow execution steps and metrics.
13
+ Prints to console and outputs structured JSON logs.
14
+ """
15
+
16
+ def __init__(self, log_dir: Optional[str] = None):
17
+ self.logger = logging.getLogger("bat_core")
18
+ self.logger.setLevel(logging.INFO)
19
+ if not self.logger.handlers:
20
+ handler = logging.StreamHandler()
21
+ formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s")
22
+ handler.setFormatter(formatter)
23
+ self.logger.addHandler(handler)
24
+
25
+ self.log_dir = Path(log_dir) if log_dir else None
26
+ if self.log_dir:
27
+ self.log_dir.mkdir(parents=True, exist_ok=True)
28
+
29
+ def log_step_start(self, step_id: str, step_name: str, action: str) -> None:
30
+ self.logger.info(f"Starting Step [{step_id}] '{step_name}' (Action: {action})")
31
+
32
+ def log_step_result(self, result: StepResult) -> None:
33
+ status_upper = result.status.upper()
34
+ if result.status == "success":
35
+ self.logger.info(
36
+ f"Completed Step [{result.step_id}] '{result.step_name}' in {result.duration_seconds:.3f}s"
37
+ )
38
+ elif result.status == "failed":
39
+ self.logger.error(
40
+ f"Failed Step [{result.step_id}] '{result.step_name}' - [{result.error_type} Error]: {result.error_message}"
41
+ )
42
+ else:
43
+ self.logger.warning(f"Skipped Step [{result.step_id}] '{result.step_name}'")
44
+
45
+ def log_execution_summary(self, context: ExecutionContext) -> None:
46
+ m = context.metrics
47
+ self.logger.info("=== Flow Execution Summary ===")
48
+ self.logger.info(f"Flow Name: {context.flow_name}")
49
+ self.logger.info(f"Total Steps: {m.total_steps} (Success: {m.successful_steps}, Failed: {m.failed_steps}, Skipped: {m.skipped_steps})")
50
+ self.logger.info(f"Total Duration: {m.total_duration_seconds:.2f}s")
51
+
52
+ if self.log_dir:
53
+ log_file = self.log_dir / f"log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
54
+ log_file.write_text(json.dumps(context.model_dump(mode="json"), indent=2), encoding="utf-8")
55
+ self.logger.info(f"Saved JSON log to: {log_file}")
@@ -0,0 +1,17 @@
1
+ """
2
+ Data models for Flow definitions and execution contexts.
3
+ """
4
+
5
+ from .flow import FlowDefinition, Step, ActionConfig, VariableDefinition, ErrorHandlerConfig
6
+ from .context import ExecutionContext, StepResult, ExecutionMetrics
7
+
8
+ __all__ = [
9
+ "FlowDefinition",
10
+ "Step",
11
+ "ActionConfig",
12
+ "VariableDefinition",
13
+ "ErrorHandlerConfig",
14
+ "ExecutionContext",
15
+ "StepResult",
16
+ "ExecutionMetrics",
17
+ ]
@@ -0,0 +1,41 @@
1
+ from datetime import datetime
2
+ from typing import Any, Dict, List, Optional
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class StepResult(BaseModel):
7
+ step_id: str
8
+ step_name: str
9
+ status: str # success, failed, skipped
10
+ start_time: datetime
11
+ end_time: datetime
12
+ duration_seconds: float
13
+ output: Optional[Any] = None
14
+ error_message: Optional[str] = None
15
+ error_type: Optional[str] = None # Technical, Business
16
+
17
+
18
+ class ExecutionMetrics(BaseModel):
19
+ total_steps: int = 0
20
+ successful_steps: int = 0
21
+ failed_steps: int = 0
22
+ skipped_steps: int = 0
23
+ total_duration_seconds: float = 0.0
24
+ hours_saved: float = 0.0
25
+ cost_saved: float = 0.0
26
+
27
+
28
+ class ExecutionContext(BaseModel):
29
+ flow_name: str
30
+ start_time: datetime = Field(default_factory=datetime.now)
31
+ variables: Dict[str, Any] = Field(default_factory=dict)
32
+ step_results: List[StepResult] = Field(default_factory=list)
33
+ metrics: ExecutionMetrics = Field(default_factory=ExecutionMetrics)
34
+ is_completed: bool = False
35
+ has_error: bool = False
36
+
37
+ def set_variable(self, name: str, value: Any) -> None:
38
+ self.variables[name] = value
39
+
40
+ def get_variable(self, name: str, default: Any = None) -> Any:
41
+ return self.variables.get(name, default)
@@ -0,0 +1,44 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class VariableDefinition(BaseModel):
6
+ name: str
7
+ value: Any
8
+ description: Optional[str] = None
9
+
10
+
11
+ class ErrorHandlerConfig(BaseModel):
12
+ on_error: str = "stop" # stop, continue, retry
13
+ max_retries: int = 0
14
+ retry_interval: float = 1.0
15
+ fallback_step_id: Optional[str] = None
16
+
17
+
18
+ class Step(BaseModel):
19
+ id: str
20
+ name: str
21
+ action: str
22
+ parameters: Dict[str, Any] = Field(default_factory=dict)
23
+ output_var: Optional[str] = None
24
+ sub_steps: Optional[List["Step"]] = None
25
+ error_handler: Optional[ErrorHandlerConfig] = None
26
+ condition: Optional[str] = None
27
+
28
+
29
+ Step.model_rebuild()
30
+
31
+
32
+ class ActionConfig(BaseModel):
33
+ name: str
34
+ action_class: str
35
+ module: str
36
+ description: Optional[str] = None
37
+
38
+
39
+ class FlowDefinition(BaseModel):
40
+ name: str
41
+ description: Optional[str] = None
42
+ version: str = "1.0.0"
43
+ variables: Dict[str, Any] = Field(default_factory=dict)
44
+ steps: List[Step] = Field(default_factory=list)
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: batautomate
3
+ Version: 0.1.0
4
+ Summary: Core execution engine and standard action libraries for BAT Automate RPA platform
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: pydantic>=2.0.0
8
+ Requires-Dist: playwright>=1.40.0
9
+ Requires-Dist: pandas>=2.0.0
10
+ Requires-Dist: openpyxl>=3.1.0
11
+ Requires-Dist: requests>=2.28.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
14
+
15
+ # bat-core
16
+
17
+ Execution Runtime Engine and Standard Action Libraries for BAT Automate RPA Platform.
18
+
19
+ ## Directory Structure
20
+
21
+ ```text
22
+ bat-core/
23
+ ├── pyproject.toml # Project configuration & dependencies
24
+ ├── requirements.txt # Package dependencies list
25
+ ├── README.md # Documentation
26
+ ├── bat_core/ # Core Python Package
27
+ │ ├── __init__.py
28
+ │ ├── cli.py # CLI Runner
29
+ │ ├── models/ # Flow & Execution Models (Pydantic)
30
+ │ │ ├── __init__.py
31
+ │ │ ├── flow.py
32
+ │ │ └── context.py
33
+ │ ├── engine/ # Flow Interpreter & Evaluator Engine
34
+ │ │ ├── __init__.py
35
+ │ │ ├── interpreter.py
36
+ │ │ ├── evaluator.py
37
+ │ │ └── logger.py
38
+ │ └── actions/ # Action Libraries & Registry
39
+ │ ├── __init__.py
40
+ │ ├── base.py
41
+ │ ├── registry.py
42
+ │ ├── logic.py
43
+ │ ├── data_excel.py
44
+ │ ├── web_playwright.py
45
+ │ └── http_api.py
46
+ ├── examples/ # Sample Flow JSON files
47
+ │ └── sample_flow.json
48
+ └── tests/ # Unit Tests
49
+ └── test_engine.py
50
+ ```
51
+
52
+ ---
53
+
54
+ ## คู่มือการใช้งานและการสั่งรันคำสั่ง (CLI Execution Guide)
55
+
56
+ โมดูล `bat-core` สามารถสั่งรันกระบวนการอัตโนมัติ (RPA Flow) ผ่าน Command Line Interface (CLI) ได้ดังนี้:
57
+
58
+ ### 1. การติดตั้งแบบ Editable (สำหรับการพัฒนา)
59
+
60
+ ติดตั้ง Package และ Dependencies ในสภาพแวดล้อม Python:
61
+ ```powershell
62
+ pip install -e .
63
+ ```
64
+
65
+ ### 2. รูปแบบคำสั่งการรันพื้นฐาน
66
+
67
+ รันผ่านสคริปต์ Python Module:
68
+ ```powershell
69
+ python -m bat_core.cli run examples/sample_flow.json
70
+ ```
71
+
72
+ หรือรันผ่านคำสั่ง CLI สั้น (เมื่อทำการติดตั้ง Package แล้ว):
73
+ ```powershell
74
+ bat-core run examples/sample_flow.json
75
+ ```
76
+
77
+ ### 3. ตัวเลือกและพารามิเตอร์เสริม (CLI Options)
78
+
79
+ - **`--vars` (Override / Inject Variables):** ส่งค่าตัวแปรจากภายนอกเข้าไปใน Flow รูปแบบ JSON String
80
+ ```powershell
81
+ python -m bat_core.cli run examples/sample_flow.json --vars "{\"target_url\": \"https://httpbin.org/get\"}"
82
+ ```
83
+
84
+ - **`--log-dir` (Save Execution Logs):** กำหนดไดเรกทอรีสำหรับบันทึกไฟล์ JSON Execution Log
85
+ ```powershell
86
+ python -m bat_core.cli run examples/sample_flow.json --log-dir "./logs"
87
+ ```
88
+
89
+ ### 4. การรันชุดทดสอบ (Unit Testing)
90
+
91
+ ทดสอบความถูกต้องของระบบและ Action ทั้งหมด:
92
+ ```powershell
93
+ python -m pytest tests/
94
+ ```
95
+
@@ -0,0 +1,25 @@
1
+ README.md
2
+ pyproject.toml
3
+ bat_core/__init__.py
4
+ bat_core/cli.py
5
+ bat_core/actions/__init__.py
6
+ bat_core/actions/base.py
7
+ bat_core/actions/data_excel.py
8
+ bat_core/actions/http_api.py
9
+ bat_core/actions/logic.py
10
+ bat_core/actions/registry.py
11
+ bat_core/actions/web_playwright.py
12
+ bat_core/engine/__init__.py
13
+ bat_core/engine/evaluator.py
14
+ bat_core/engine/interpreter.py
15
+ bat_core/engine/logger.py
16
+ bat_core/models/__init__.py
17
+ bat_core/models/context.py
18
+ bat_core/models/flow.py
19
+ batautomate.egg-info/PKG-INFO
20
+ batautomate.egg-info/SOURCES.txt
21
+ batautomate.egg-info/dependency_links.txt
22
+ batautomate.egg-info/entry_points.txt
23
+ batautomate.egg-info/requires.txt
24
+ batautomate.egg-info/top_level.txt
25
+ tests/test_engine.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bat-core = bat_core.cli:main
@@ -0,0 +1,8 @@
1
+ pydantic>=2.0.0
2
+ playwright>=1.40.0
3
+ pandas>=2.0.0
4
+ openpyxl>=3.1.0
5
+ requests>=2.28.0
6
+
7
+ [dev]
8
+ pytest>=7.0.0
@@ -0,0 +1 @@
1
+ bat_core
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "batautomate"
7
+ version = "0.1.0"
8
+ description = "Core execution engine and standard action libraries for BAT Automate RPA platform"
9
+ readme = {file = "README.md", content-type = "text/markdown"}
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "pydantic>=2.0.0",
13
+ "playwright>=1.40.0",
14
+ "pandas>=2.0.0",
15
+ "openpyxl>=3.1.0",
16
+ "requests>=2.28.0"
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest>=7.0.0"
22
+ ]
23
+
24
+ [project.scripts]
25
+ bat-core = "bat_core.cli:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,54 @@
1
+ import json
2
+ from pathlib import Path
3
+ import pytest
4
+ from bat_core.models.flow import FlowDefinition, Step
5
+ from bat_core.engine.interpreter import FlowInterpreter
6
+ from bat_core.engine.evaluator import VariableEvaluator
7
+
8
+
9
+ def test_variable_evaluator():
10
+ vars_ctx = {"name": "BAT", "details": {"version": "1.0"}}
11
+ val = VariableEvaluator.evaluate_value("Hello ${name} v${details.version}", vars_ctx)
12
+ assert val == "Hello BAT v1.0"
13
+
14
+
15
+ def test_interpreter_basic_run():
16
+ flow = FlowDefinition(
17
+ name="Test Flow",
18
+ variables={"msg": "hello"},
19
+ steps=[
20
+ Step(
21
+ id="s1",
22
+ name="Set Variable",
23
+ action="logic.set_variable",
24
+ parameters={"name": "res", "value": "${msg}_world"},
25
+ output_var="res"
26
+ )
27
+ ]
28
+ )
29
+
30
+ interpreter = FlowInterpreter()
31
+ ctx = interpreter.run_flow(flow)
32
+
33
+ assert ctx.is_completed is True
34
+ assert ctx.has_error is False
35
+ assert ctx.get_variable("res") == "hello_world"
36
+ assert len(ctx.step_results) == 1
37
+ assert ctx.step_results[0].status == "success"
38
+
39
+
40
+ def test_interpreter_load_sample_flow_json():
41
+ json_path = Path(__file__).parent.parent / "examples" / "sample_flow.json"
42
+ assert json_path.exists()
43
+
44
+ content = json_path.read_text(encoding="utf-8")
45
+ raw_json = json.loads(content)
46
+ flow_def = FlowDefinition.model_validate(raw_json)
47
+
48
+ interpreter = FlowInterpreter()
49
+ ctx = interpreter.run_flow(flow_def)
50
+
51
+ assert ctx.is_completed is True
52
+ assert ctx.has_error is False
53
+ assert len(ctx.step_results) == 3
54
+