Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
88 changes: 88 additions & 0 deletions botcity/core/application/functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import time
from pywinauto.timings import TimeoutError
from pywinauto.findwindows import ElementNotFoundError
from pywinauto.application import Application, WindowSpecification
from .utils import Backend
from .. import config


def connect(backend=Backend.WIN_32, timeout=60000, **connection_selectors) -> Application:
"""
Connects to an instance of an open application.
Use this method to be able to access application windows and elements.

Args:
backend (Backend, optional): The accessibility technology defined in the Backend class
that could be used for your application. Defaults to Backend.WIN_32 ('win32').
timeout (int, optional): Maximum wait time (ms) to wait for connection.
Defaults to 60000ms (60s).
**connection_selectors: Attributes that can be used to connect to an application.
[See more details about the available selectors](https://documentation.botcity.dev).

Returns
app (Application): The Application instance.
"""
connect_exception = None
start_time = time.time()
while True:
elapsed_time = (time.time() - start_time) * 1000
if elapsed_time > timeout:
if connect_exception:
raise connect_exception
return None
try:
app = Application(backend=backend).connect(**connection_selectors)
return app
except Exception as e:
connect_exception = e
time.sleep(config.DEFAULT_SLEEP_AFTER_ACTION/1000.0)


def find_window(app: Application, waiting_time=10000, **selectors) -> WindowSpecification:
"""
Find a window of the currently connected application using the available selectors.

Args:
app (Application): The connected application.
waiting_time (int, optional): Maximum wait time (ms) to search for a hit.
Defaults to 10000ms (10s).
**selectors: Attributes that can be used to filter an element.
[See more details about the available selectors](https://documentation.botcity.dev).

Returns
dialog (WindowSpecification): The window or control found.
"""
try:
dialog = app.window(**selectors)
dialog.wait(wait_for='exists ready', timeout=waiting_time / 1000.0)
return dialog
except (TimeoutError, ElementNotFoundError):
return None


def find_element(app: Application, from_parent_window: WindowSpecification = None,
waiting_time=10000, **selectors) -> WindowSpecification:
"""
Find a element of the currently connected application using the available selectors.
You can pass the context window where the element is contained.

Args:
app (Application): The connected application.
from_parent_window (WindowSpecification, optional): The element's parent window.
waiting_time (int, optional): Maximum wait time (ms) to search for a hit.
Defaults to 10000ms (10s).
**selectors: Attributes that can be used to filter an element.
[See more details about the available selectors](https://documentation.botcity.dev).

Returns
element (WindowSpecification): The element/control found.
"""
try:
if not from_parent_window:
element = find_window(app, waiting_time, **selectors)
else:
element = from_parent_window.child_window(**selectors)
element.wait(wait_for='exists ready', timeout=waiting_time / 1000.0)
return element
except (TimeoutError, ElementNotFoundError):
return None
53 changes: 53 additions & 0 deletions botcity/core/application/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import enum
import platform
from functools import wraps


class Backend(str, enum.Enum):
"""
Supported accessibility technologies.

Attributes:
WIN_32 (str): 'win32' backend
UIA (str): 'uia' backend
"""
WIN_32 = "win32"
UIA = "uia"


def if_windows_os(func):
"""
Decorator which raises if the OS is not Windows.

Args:
func (callable): The function to be wrapped

Returns:
wrapper (callable): The decorated function
"""
@wraps(func)
def wrapper(self, *args, **kwargs):
if platform.system() == "Windows":
return func(self, *args, **kwargs)
raise ValueError(
f'You can connect to an application on Windows OS only. Cannot invoke {func.__name__}.'
)
return wrapper


def if_app_connected(func):
"""
Decorator which raises if no apps connected.

Args:
func (callable): The function to be wrapped

Returns:
wrapper (callable): The decorated function
"""
@wraps(func)
def wrapper(self, *args, **kwargs):
if self.app is None:
raise ValueError('No applications connected. Invoke connect_to_app first.')
return func(self, *args, **kwargs)
return wrapper
90 changes: 90 additions & 0 deletions botcity/core/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@

from . import config, cv2find, os_compat

try:
from pywinauto.application import Application, WindowSpecification
from .application.functions import connect, find_window, find_element
except ImportError:
pass

from .application.utils import Backend, if_windows_os, if_app_connected

try:
from botcity.maestro import BotMaestroSDK
MAESTRO_AVAILABLE = True
Expand All @@ -35,6 +43,7 @@ class DesktopBot(BaseBot):

def __init__(self):
super().__init__()
self._app = None
self.state = State()
self.maestro = BotMaestroSDK() if MAESTRO_AVAILABLE else None
self._interval = 0.005 if platform.system() == "Darwin" else 0.0
Expand Down Expand Up @@ -76,6 +85,26 @@ def __init__(self):
self.moveAndRightClick = self.right_click
pyperclip.determine_clipboard()

@property
def app(self):
"""
The connected application instance to be used.

Returns:
app (Application): The connected Application instance.
"""
return self._app

@app.setter
def app(self, app):
"""
The connected application instance to be used.

Args:
app (Application): The connected application to be used.
"""
self._app = app

##########
# Display
##########
Expand Down Expand Up @@ -1503,3 +1532,64 @@ def sleep(self, interval):

"""
self.wait(interval)

#############
# Application
#############

@if_windows_os
def connect_to_app(self, backend=Backend.WIN_32, timeout=60000, **connection_selectors) -> 'Application':
"""
Connects to an instance of an open application.
Use this method to be able to access application windows and elements.

Args:
backend (Backend, optional): The accessibility technology defined in the Backend class
that could be used for your application. Defaults to Backend.WIN_32 ('win32').
timeout (int, optional): Maximum wait time (ms) to wait for connection.
Defaults to 60000ms (60s).
**connection_selectors: Attributes that can be used to connect to an application.
[See more details about the available selectors](https://documentation.botcity.dev).

Returns
app (Application): The Application instance.
"""
self.app = connect(backend, timeout, **connection_selectors)
return self.app

@if_app_connected
def find_app_window(self, waiting_time=10000, **selectors) -> 'WindowSpecification':
"""
Find a window of the currently connected application using the available selectors.

Args:
waiting_time (int, optional): Maximum wait time (ms) to search for a hit.
Defaults to 10000ms (10s).
**selectors: Attributes that can be used to filter an element.
[See more details about the available selectors](https://documentation.botcity.dev).

Returns
dialog (WindowSpecification): The window or control found.
"""
dialog = find_window(self.app, waiting_time, **selectors)
return dialog

@if_app_connected
def find_app_element(self, from_parent_window: 'WindowSpecification' = None,
waiting_time=10000, **selectors) -> 'WindowSpecification':
"""
Find a element of the currently connected application using the available selectors.
You can pass the context window where the element is contained.

Args:
from_parent_window (WindowSpecification, optional): The element's parent window.
waiting_time (int, optional): Maximum wait time (ms) to search for a hit.
Defaults to 10000ms (10s).
**selectors: Attributes that can be used to filter an element.
[See more details about the available selectors](https://documentation.botcity.dev).

Returns
element (WindowSpecification): The element/control found.
"""
element = find_element(self.app, from_parent_window, waiting_time, **selectors)
return element
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pyscreeze==0.1.27
pyperclip
opencv-python
python-xlib
pywinauto
7 changes: 7 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@
from os import path
from codecs import open
import versioneer
import platform

cur_dir = path.abspath(path.dirname(__file__))

with open(path.join(cur_dir, 'requirements.txt'), 'r') as f:
requirements = f.read().split()

if platform.system() != "Windows":
try:
requirements.remove("pywinauto")
except ValueError:
pass

setup(
name='botcity-framework-core',
version=versioneer.get_version(),
Expand Down