Skip to content

Commit a2980a0

Browse files
authored
Merge pull request #5 from itk-dev-rpa/release-v1
Release v1
2 parents 305822b + 400395e commit a2980a0

22 files changed

+286
-228
lines changed

.github/workflows/pylint.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Pylint
1+
name: Linting
22

33
on: [push]
44

@@ -14,10 +14,18 @@ jobs:
1414
uses: actions/setup-python@v3
1515
with:
1616
python-version: ${{ matrix.python-version }}
17+
1718
- name: Install dependencies
1819
run: |
1920
python -m pip install --upgrade pip
2021
pip install pylint
22+
pip install flake8
23+
pip install .
24+
2125
- name: Analysing the code with pylint
2226
run: |
2327
pylint --rcfile=.pylintrc $(git ls-files '*.py')
28+
29+
- name: Analysing the code with flake8
30+
run: |
31+
flake8 --extend-ignore=E501,E251 $(git ls-files '*.py')

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This repo is meant to be used as a template for robots made for [OpenOrchestrato
66

77
1. To use the template simply use the repo as a template as shown [here](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template).
88

9-
2. Fill out the requirements.txt file with all packages needed by the robot.
9+
2. Fill out the dependencies in the pyproject.toml file with all packages needed by the robot.
1010

1111
3. Implement all functions in the files:
1212
* src/initialize.py
@@ -15,12 +15,12 @@ This repo is meant to be used as a template for robots made for [OpenOrchestrato
1515
* src/process.py
1616
* Feel free to add more files as needed.
1717

18-
4. Make sure the smtp setup in error_screenshot.py is set up to your needs.
18+
4. Change config.py to your needs.
1919

20-
When the robot is run from OpenOrchestrator the main.bat file is run.
21-
main.bat does a few things:
20+
When the robot is run from OpenOrchestrator the main.py file is run.
21+
main.py does a few things:
2222
1. A virtual environment is automatically setup with the required packages.
23-
2. The framework is called passing on all arguments needed by [OpenOrchestratorConnection](https://github.com/itk-dev-rpa/OpenOrchestratorConnection).
23+
2. The framework is called passing on all arguments needed by [OpenOrchestrator](https://github.com/itk-dev-rpa/OpenOrchestrator).
2424

2525
## Requirements
2626
Minimum python version 3.10

Robot-Framework.drawio

Lines changed: 32 additions & 32 deletions
Large diffs are not rendered by default.

main.bat

Lines changed: 0 additions & 16 deletions
This file was deleted.

main.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""The main file of the robot which will install all requirements in
2+
a virtual environment and then start the actual process.
3+
"""
4+
5+
import subprocess
6+
import os
7+
import sys
8+
9+
script_directory = os.path.dirname(os.path.realpath(__file__))
10+
os.chdir(script_directory)
11+
12+
subprocess.run("python -m venv .venv", check=True)
13+
subprocess.run(r'.venv\Scripts\pip install .', check=True)
14+
15+
command_args = [r".venv\Scripts\python", "-m", "robot_framework"] + sys.argv[1:]
16+
17+
subprocess.run(command_args, check=True)

pyproject.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[build-system]
2+
requires = ["setuptools>=65.0"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "robot_framework"
7+
version = "0.0.1"
8+
authors = [
9+
{ name="ITK Development", email="[email protected]" },
10+
]
11+
readme = "README.md"
12+
requires-python = ">=3.11"
13+
classifiers = [
14+
"Programming Language :: Python :: 3",
15+
"License :: OSI Approved :: MIT License",
16+
"Operating System :: Microsoft :: Windows",
17+
]
18+
dependencies = [
19+
"OpenOrchestrator == 1.*",
20+
"Pillow == 9.5.0",
21+
]

requirements.txt

Lines changed: 0 additions & 6 deletions
This file was deleted.

robot_framework/__init__.py

Whitespace-only changes.

robot_framework/__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""The entry point of the process."""
2+
3+
from robot_framework.framework import main
4+
main()

robot_framework/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""This module contains configuration constants used across the framework"""
2+
3+
# The number of times the robot retries on an error before terminating.
4+
MAX_RETRY_COUNT = 3
5+
6+
# Error screenshot config
7+
SMTP_SERVER = "smtp.aarhuskommune.local"
8+
SMTP_PORT = 25
9+
SCREENSHOT_SENDER = "[email protected]"

robot_framework/error_screenshot.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""This module has functionality to send error screenshots via smtp."""
2+
3+
import smtplib
4+
from email.message import EmailMessage
5+
import base64
6+
import traceback
7+
from io import BytesIO
8+
9+
from PIL import ImageGrab
10+
11+
from robot_framework import config
12+
13+
14+
def send_error_screenshot(to_address: str | list[str], exception: Exception, process_name: str):
15+
"""Sends an email with an error report, including a screenshot, when an exception occurs.
16+
Configuration details such as SMTP server, port, sender email, etc., should be set in 'config' module.
17+
18+
Args:
19+
to_address: Email address or list of addresses to send the error report.
20+
exception: The exception that triggered the error.
21+
process_name: Name of the process from OpenOrchestrator.
22+
"""
23+
# Create message
24+
msg = EmailMessage()
25+
msg['to'] = to_address
26+
msg['from'] = config.SCREENSHOT_SENDER
27+
msg['subject'] = f"Error screenshot: {process_name}"
28+
29+
# Take screenshot and convert to base64
30+
screenshot = ImageGrab.grab()
31+
buffer = BytesIO()
32+
screenshot.save(buffer, format='PNG')
33+
screenshot_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
34+
35+
# Create an HTML message with the exception and screenshot
36+
html_message = f"""
37+
<html>
38+
<body>
39+
<p>Error type: {type(exception).__name__}</p>
40+
<p>Error message: {exception}</p>
41+
<p>{traceback.format_exc()}</p>
42+
<img src="data:image/png;base64,{screenshot_base64}" alt="Screenshot">
43+
</body>
44+
</html>
45+
"""
46+
47+
msg.set_content("Please enable HTML to view this message.")
48+
msg.add_alternative(html_message, subtype='html')
49+
50+
# Send message
51+
with smtplib.SMTP(config.SMTP_SERVER, config.SMTP_PORT) as smtp:
52+
smtp.starttls()
53+
smtp.send_message(msg)

robot_framework/framework.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""This module is the primary module of the robot framework. It collects the functionality of the rest of the framework."""
2+
3+
import traceback
4+
import sys
5+
6+
from OpenOrchestrator.orchestrator_connection.connection import OrchestratorConnection
7+
8+
from robot_framework import initialize
9+
from robot_framework import get_constants
10+
from robot_framework import reset
11+
from robot_framework import error_screenshot
12+
from robot_framework import process
13+
from robot_framework import config
14+
15+
16+
def main():
17+
"""The entry point for the framework. Should be called as the first thing when running the robot."""
18+
orchestrator_connection = OrchestratorConnection.create_connection_from_args()
19+
sys.excepthook = log_exception(orchestrator_connection)
20+
21+
orchestrator_connection.log_trace("Robot Framework started.")
22+
initialize.initialize(orchestrator_connection)
23+
constants = get_constants.get_constants(orchestrator_connection)
24+
25+
error_count = 0
26+
for _ in range(config.MAX_RETRY_COUNT):
27+
try:
28+
reset.reset(orchestrator_connection)
29+
process.process(orchestrator_connection)
30+
break
31+
32+
# If any business rules are broken the robot should stop entirely.
33+
except BusinessError as error:
34+
orchestrator_connection.log_error(f"BusinessError: {error}\nTrace: {traceback.format_exc()}")
35+
error_screenshot.send_error_screenshot(constants.error_email, error, orchestrator_connection.process_name)
36+
break
37+
38+
# We actually want to catch all exceptions possible here.
39+
# pylint: disable-next = broad-exception-caught
40+
except Exception as error:
41+
error_count += 1
42+
error_type = type(error).__name__
43+
orchestrator_connection.log_error(f"Error caught during process. Total number of errors caught: {error_count}. {error_type}: {error}\nTrace: {traceback.format_exc()}")
44+
error_screenshot.send_error_screenshot(constants.error_email, error, orchestrator_connection.process_name)
45+
46+
reset.clean_up(orchestrator_connection)
47+
reset.close_all(orchestrator_connection)
48+
reset.kill_all(orchestrator_connection)
49+
50+
51+
def log_exception(orchestrator_connection: OrchestratorConnection) -> callable:
52+
"""Creates a function to be used as an exception hook that logs any uncaught exception in OpenOrchestrator.
53+
54+
Args:
55+
orchestrator_connection: The connection to OpenOrchestrator.
56+
57+
Returns:
58+
callable: A function that can be assigned to sys.excepthook.
59+
"""
60+
def inner(exception_type, value, traceback_string):
61+
orchestrator_connection.log_error(f"Uncaught Exception:\nType: {exception_type}\nValue: {value}\nTrace: {traceback_string}")
62+
return inner
63+
64+
65+
class BusinessError(Exception):
66+
"""An empty exception used to identify errors caused by breaking business rules"""

robot_framework/get_constants.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""This module defines how the robot should receive and store constants/credentials from OpenOrchestrator."""
2+
3+
from dataclasses import dataclass
4+
5+
from OpenOrchestrator.orchestrator_connection.connection import OrchestratorConnection
6+
7+
8+
@dataclass(kw_only=True)
9+
class Constants:
10+
"""An object for holding any robot specific constants. Expand as needed"""
11+
error_email: str
12+
13+
14+
def get_constants(orchestrator_connection: OrchestratorConnection) -> Constants:
15+
"""Get all constants used by the robot."""
16+
orchestrator_connection.log_trace("Getting constants.")
17+
18+
constants = Constants(
19+
error_email = orchestrator_connection.get_constant("Error Email").value
20+
)
21+
22+
return constants

robot_framework/initialize.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""This module defines any initial processes to run when the robot starts."""
2+
3+
from OpenOrchestrator.orchestrator_connection.connection import OrchestratorConnection
4+
5+
6+
def initialize(orchestrator_connection: OrchestratorConnection) -> None:
7+
"""Do all custom startup initializations of the robot."""
8+
orchestrator_connection.log_trace("Initializing.")

robot_framework/process.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""This module contains the main process of the robot."""
2+
3+
from OpenOrchestrator.orchestrator_connection.connection import OrchestratorConnection
4+
5+
6+
def process(orchestrator_connection: OrchestratorConnection) -> None:
7+
"""Do the primary process of the robot."""
8+
orchestrator_connection.log_trace("Running process.")

robot_framework/reset.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""This module handles resetting the state of the computer so the robot can work with a clean slate."""
2+
3+
from OpenOrchestrator.orchestrator_connection.connection import OrchestratorConnection
4+
5+
6+
def reset(orchestrator_connection: OrchestratorConnection) -> None:
7+
"""Clean up, close/kill all programs and start them again. """
8+
orchestrator_connection.log_trace("Resetting.")
9+
clean_up(orchestrator_connection)
10+
close_all(orchestrator_connection)
11+
kill_all(orchestrator_connection)
12+
open_all(orchestrator_connection)
13+
14+
15+
def clean_up(orchestrator_connection: OrchestratorConnection) -> None:
16+
"""Do any cleanup needed to leave a blank slate."""
17+
orchestrator_connection.log_trace("Doing cleanup.")
18+
19+
20+
def close_all(orchestrator_connection: OrchestratorConnection) -> None:
21+
"""Gracefully close all applications used by the robot."""
22+
orchestrator_connection.log_trace("Closing all applications.")
23+
24+
25+
def kill_all(orchestrator_connection: OrchestratorConnection) -> None:
26+
"""Forcefully close all applications used by the robot."""
27+
orchestrator_connection.log_trace("Killing all applications.")
28+
29+
30+
def open_all(orchestrator_connection: OrchestratorConnection) -> None:
31+
"""Open all programs used by the robot."""
32+
orchestrator_connection.log_trace("Opening all applications.")

src/error_screenshot.py

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)