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.
- batautomate-0.1.0/PKG-INFO +95 -0
- batautomate-0.1.0/README.md +81 -0
- batautomate-0.1.0/bat_core/__init__.py +5 -0
- batautomate-0.1.0/bat_core/actions/__init__.py +17 -0
- batautomate-0.1.0/bat_core/actions/base.py +20 -0
- batautomate-0.1.0/bat_core/actions/data_excel.py +39 -0
- batautomate-0.1.0/bat_core/actions/http_api.py +36 -0
- batautomate-0.1.0/bat_core/actions/logic.py +24 -0
- batautomate-0.1.0/bat_core/actions/registry.py +32 -0
- batautomate-0.1.0/bat_core/actions/web_playwright.py +27 -0
- batautomate-0.1.0/bat_core/cli.py +56 -0
- batautomate-0.1.0/bat_core/engine/__init__.py +13 -0
- batautomate-0.1.0/bat_core/engine/evaluator.py +55 -0
- batautomate-0.1.0/bat_core/engine/interpreter.py +134 -0
- batautomate-0.1.0/bat_core/engine/logger.py +55 -0
- batautomate-0.1.0/bat_core/models/__init__.py +17 -0
- batautomate-0.1.0/bat_core/models/context.py +41 -0
- batautomate-0.1.0/bat_core/models/flow.py +44 -0
- batautomate-0.1.0/batautomate.egg-info/PKG-INFO +95 -0
- batautomate-0.1.0/batautomate.egg-info/SOURCES.txt +25 -0
- batautomate-0.1.0/batautomate.egg-info/dependency_links.txt +1 -0
- batautomate-0.1.0/batautomate.egg-info/entry_points.txt +2 -0
- batautomate-0.1.0/batautomate.egg-info/requires.txt +8 -0
- batautomate-0.1.0/batautomate.egg-info/top_level.txt +1 -0
- batautomate-0.1.0/pyproject.toml +25 -0
- batautomate-0.1.0/setup.cfg +4 -0
- batautomate-0.1.0/tests/test_engine.py +54 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -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,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
|
+
|