Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
89731d0
1) Refactored documentation
lexter0705 May 3, 2026
0b6316b
1) Refactored documentation
lexter0705 May 3, 2026
932c537
1) Added github pages build workflow
lexter0705 May 3, 2026
d590321
1) Moved debuggers to instructions module
lexter0705 May 3, 2026
66bf86a
1) Updated docs requirements.txt
lexter0705 May 3, 2026
96ce2a9
1) Added UniqueAlgorithm
lexter0705 May 9, 2026
9528b91
1) Renamed BaseDebugger.clear_contex method to clear_context
lexter0705 May 9, 2026
4958d7d
1) Now, in all algorithms, the debugger is created using a boolean va…
lexter0705 May 9, 2026
4713259
1) The initialization of text_getter argument in the OCR instructions…
lexter0705 May 9, 2026
805475f
1) The GetText.local_answer method has been renamed to global_answer.
lexter0705 May 9, 2026
8f5d8ba
1) Fixed tests\tests_instructions\test_ocr.py
lexter0705 May 9, 2026
b929318
1) Removed KeyboardKeyCode from api
lexter0705 May 9, 2026
f402317
1) Refactored tests
lexter0705 May 10, 2026
b2664cf
1) Added attributes field to IdsAlgorithm and NameAlgorithm. Attribut…
lexter0705 May 11, 2026
301daa1
1) Refactored documentation
lexter0705 May 11, 2026
d190e1f
1) Fixed PaddleOCRReader
lexter0705 May 12, 2026
c6eff94
1) Fixed PaddleOCR tests
lexter0705 May 12, 2026
9b79a0f
1) Fixed documentation
lexter0705 May 12, 2026
4b6fa7b
Update README.md
lexter0705 May 13, 2026
01d06a4
1) Fixed ChatTTS speaker and ChatTTS tests
lexter0705 May 14, 2026
ade8f58
1) Fixed algorithm debugger constructor argument.
lexter0705 May 15, 2026
22dc3a8
1) Added bitrate to speakers result
lexter0705 May 15, 2026
347c690
Merge remote-tracking branch 'origin/master'
lexter0705 May 15, 2026
fe691f2
1) Added PressKeyDown and PressKeyUp instructions
lexter0705 May 17, 2026
b0f6001
1) Added TimeoutException class
lexter0705 May 17, 2026
47d223e
Delete build/lib/apparser directory
lexter0705 May 18, 2026
f45104c
1) Removed ui argument from SpeakInstruction.perform method
lexter0705 May 18, 2026
58082d5
1) Added docstrings to apparser.cv module
lexter0705 May 18, 2026
be6db37
1) Added MouseDown and MouseUp instructions
May 19, 2026
f7e7919
1) Fixed WaitText docstrings
May 19, 2026
7e868ab
1) Fixed docstrings and annotation
lexter0705 May 19, 2026
c083456
1) Refactored App class
lexter0705 May 21, 2026
d69e081
1) Fixed CoordinatesUi class
lexter0705 May 24, 2026
347c76f
1) Changed keyboard and mouse library to pyautogui
lexter0705 May 25, 2026
2085db5
1) Added WindowByDisplayUi class
lexter0705 May 28, 2026
1fa674e
1) Added tests to WindowByDisplayUi, TimeoutException, MouseDown, Mou…
lexter0705 May 28, 2026
385aa0b
1) Refactored logo
lexter0705 May 30, 2026
066c131
1) Refactored logo
lexter0705 May 30, 2026
b079e84
1) Refactored logo
lexter0705 May 30, 2026
ab684a9
1) Refactored logo
lexter0705 May 30, 2026
b90d1c8
1) Fixed documentation
lexter0705 Jun 1, 2026
3298d63
1) Removed text_readers and cv examples
lexter0705 Jun 1, 2026
128b851
1) Updated about text
lexter0705 Jun 1, 2026
e093e70
1) Updated pyproject.toml
lexter0705 Jun 1, 2026
c4b3d0f
1) Updated pyproject.toml
lexter0705 Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
30 changes: 30 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: docs

on:
push:
branches:
- main
- master
workflow_dispatch:

permissions:
contents: write

jobs:
docs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.14'
- run: python -m pip install --upgrade pip
- run: pip install -r docs/requirements.txt
- run: sphinx-build -b html -E -a ./docs ./docs/_build/html
- run: New-Item docs/_build/html/.nojekyll -ItemType File -Force
- uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ github.token }}
publish_branch: gh-pages
publish_dir: docs/_build/html
force_orphan: true
22 changes: 9 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
<img src="apparser.svg" alt="" width="40%">
<img src="https://raw.githubusercontent.com/apparser-development/apparser/refs/heads/master/apparser.svg" alt="" width="40%">

[![License - BSD 3-Clause](https://img.shields.io/pypi/l/appwindows.svg)](https://github.com/lexter0705/appwindows/blob/master/LICENSE.md) [![unit_tests](https://github.com/lexter0705/appwindows/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/lexter0705/appwindows/actions/workflows/unit_tests.yml)
[![Documentation](https://img.shields.io/badge/docs-pages-green)](https://apparser-development.github.io/apparser/)
[![unit_tests](https://github.com/apparser-development/appwindows/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/apparser-development/appwindows/actions/workflows/unit_tests.yml)
<br>
[![PyPI Downloads](https://static.pepy.tech/personalized-badge/appwindows?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/appwindows)
[![Documentation](https://img.shields.io/badge/docs-gitbook-green)](https://apparser.gitbook.io/appwindows)
<br>
[![PyPi](https://img.shields.io/badge/PyPi-link-green)](https://pypi.org/project/appwindows/)
[![Github](https://img.shields.io/badge/github-repo-green)](https://github.com/lexter0705/appwindows)
[![Issues](https://img.shields.io/badge/github-issues-green)](https://github.com/lexter0705/appwindows/issues)
[![Github](https://img.shields.io/badge/github-repo-green)](https://github.com/apparser-development/apparser)
[![Issues](https://img.shields.io/badge/github-issues-green)](https://github.com/apparser-development/apparser/issues)

# Apparser
The apparser library is designed for testing and managing computer programs.

Apparser is a Python library designed for automating desktop applications and managing UI interfaces using artificial intelligence, such as OCR or object detection models.
# Install
```bash
pip install apparser
Expand All @@ -31,14 +27,14 @@ algorithm = Algorithm([
WriteText("Hello World") # Write text
])

app = App("cmd.exe")
app = App("notepad", window_title="Notepad")

algorithm.perform(app.ui)
```

# Docs
All documentation <a href="#">here</a> <br>
Link to <a href="https://pypi.org/project/appwindows/">PyPi</a>
All documentation <a href="https://apparser-development.github.io/apparser/">here</a> <br>
Link to <a href="https://pypi.org/project/apparser/">PyPi</a>

# For Developers
1) If something doesn't work - open issue.
Expand Down
16 changes: 5 additions & 11 deletions apparser.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions apparser/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from apparser.core.app import App
from apparser.core.ui import BaseUi, DesktopUi, CoordinatesUi, WindowUi
from apparser.core.ui import BaseUi, DesktopUi, CoordinatesUi, WindowUi, WindowByDisplayUi

__all__ = ["App", "BaseUi", "DesktopUi", "CoordinatesUi", "WindowUi"]
__all__ = ["App", "BaseUi", "DesktopUi", "CoordinatesUi", "WindowUi", "WindowByDisplayUi"]
57 changes: 41 additions & 16 deletions apparser/core/app.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,89 @@
import os
import subprocess
import time

from appwindows import get_finder
from appwindows.exceptions import WindowDoesNotFoundException, WindowDoesNotValidException

from apparser.core.ui.window import WindowUi, BaseUi
from apparser.geometry import Size
from apparser.core.ui import WindowUi, BaseUi


class App:
"""Manage an application process and its UI wrapper."""

def __init__(self, path_to_exe: str,
window_title: str,
window_size: Size = Size(900, 900),
window_title: str | None = None,
timeout: float = 1):
"""Initialize an application controller.

:param path_to_exe: Path to the executable file.
:type path_to_exe: str
:param window_title: Title of the window to attach to.
:type window_title: str
:param window_size: Initial size applied to the window.
:type window_size: Size
:param timeout: Delay before the window lookup starts.
:type timeout: float
:raises TypeError: If any argument has an invalid type.
"""
if not isinstance(path_to_exe, str):
raise TypeError('path_to_exe must be a string')

if not isinstance(window_title, str):
if window_title is not None and not isinstance(window_title, str):
raise TypeError('window_title must be a string')

if not isinstance(window_size, Size):
raise TypeError('window_size must be a Size')

if not (isinstance(timeout, float) or isinstance(timeout, int)):
raise TypeError('timeout must be a number')

self.__window_finder = get_finder()
self.__process: subprocess.Popen | None = None
self.__path = path_to_exe
self.__timeout = timeout
self.__window_size: Size = window_size
self.__window_title_name: str = window_title
self.__ui: BaseUi | None = None
self.start_app()

def __find_window_by_title(self):
try:
window = self.__window_finder.get_window_by_title(self.__window_title_name)
self.__ui = WindowUi(window)
except WindowDoesNotFoundException:
pass

def __find_window_by_process_id(self, process_id: int):
try:
window = self.__window_finder.get_window_by_process_id(process_id)
self.__ui = WindowUi(window)
except WindowDoesNotFoundException:
pass

def start_app(self):
"""Start the application process and bind its UI."""
"""Start the application process and bind its UI.

:raises WindowDoesNotValidException: If the window is not found or the application cannot be opened.
"""
if self.__window_title_name is not None:
self.__find_window_by_title()
if self.__ui is not None:
return
window_processes = [i.get_process_id() for i in get_finder().get_all_windows()]
self.__process = subprocess.Popen([self.__path])
time.sleep(self.__timeout)
window = self.__window_finder.get_window_by_title(self.__window_title_name)
self.__ui = WindowUi(window)
self.__ui.window.resize(self.__window_size)
self.__find_window_by_process_id(os.getpid())
for i in get_finder().get_all_windows():
if self.__ui is not None:
return
if i.get_process_id() not in window_processes:
self.__find_window_by_process_id(i.get_process_id())
if self.__ui is not None:
return
self.__find_window_by_title()
if self.__ui is not None:
raise WindowDoesNotValidException()

def stop_app(self):
"""Close the application window and stop the process."""
self.ui.window.close()
self.__process.kill()
if self.__process is not None:
self.__process.kill()

@property
def ui(self) -> BaseUi:
Expand Down
4 changes: 3 additions & 1 deletion apparser/core/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
from apparser.core.ui.desktop import DesktopUi
from apparser.core.ui.coordinates import CoordinatesUi
from apparser.core.ui.window import WindowUi
from apparser.core.ui.window_by_display import WindowByDisplayUi

__all__ = ["BaseUi", "DesktopUi", "CoordinatesUi", "WindowUi"]

__all__ = ["BaseUi", "DesktopUi", "CoordinatesUi", "WindowUi", "WindowByDisplayUi"]
85 changes: 51 additions & 34 deletions apparser/core/ui/coordinates.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,54 @@


class CoordinatesUi(BaseUi):
"""Represent a UI region inside another UI context."""

def __init__(self,
from_ui: BaseUi,
left_top_point: Point | RelativelyPoint,
size: Size):
"""Represent a UI region defined by two points inside another UI context."""

def __init__(
self,
from_ui: BaseUi,
point_one: Point | RelativelyPoint,
point_two: Point | RelativelyPoint
):
"""Initialize a nested coordinate-based UI context.

:param from_ui: Source UI used as a parent context.
:type from_ui: BaseUi
:param left_top_point: Top-left point of the nested region.
:type left_top_point: Point | RelativelyPoint
:param size: Size of the nested region.
:type size: Size
:param point_one: First point of the nested region.
:type point_one: Point | RelativelyPoint
:param point_two: Second point of the nested region or region size.
:type point_two: Point | RelativelyPoint | Size
:raises TypeError: If any argument has an invalid type.
"""
if not isinstance(from_ui, BaseUi):
raise TypeError('from_ui must be Ui')
raise TypeError('from_ui must be BaseUi')

if not (isinstance(left_top_point, Point) or isinstance(left_top_point, RelativelyPoint)):
raise TypeError('left_top_point must be Point or RelativelyPoint')
if not isinstance(point_one, (Point, RelativelyPoint)):
raise TypeError('point1 must be Point or RelativelyPoint')

if not isinstance(size, Size):
raise TypeError('size must be Size')
elif not isinstance(point_two, (Point, RelativelyPoint)):
raise TypeError('point2 must be Point, RelativelyPoint')

self.__from_ui = from_ui
self.__left_top_point = left_top_point
self.__size = size

def __get_left_top_local_point(self) -> Point:
if isinstance(self.__left_top_point, RelativelyPoint):
return self.__from_ui.point_to_local(self.__from_ui.point_to_global(self.__left_top_point))
return self.__left_top_point
self.__point_one = point_one
self.__point_two = point_two

def __point_to_main_ui_local(self, point: Point | RelativelyPoint) -> Point:
if isinstance(point, RelativelyPoint):
return self.__from_ui.point_to_local(self.__from_ui.point_to_global(point))
return point

def __get_local_bounds(self) -> tuple[Point, Point]:
point1 = self.__point_to_main_ui_local(self.__point_one)
point2 = self.__point_to_main_ui_local(self.__point_two)
left_top_point = Point(
min(point1.x, point2.x),
min(point1.y, point2.y),
)
right_bottom_point = Point(
max(point1.x, point2.x),
max(point1.y, point2.y),
)
return left_top_point, right_bottom_point

@singledispatchmethod
def point_to_global(self, coordinates: Point | RelativelyPoint) -> Point:
Expand All @@ -55,14 +70,17 @@ def point_to_global(self, coordinates: Point | RelativelyPoint) -> Point:
raise NotImplementedError()

@point_to_global.register(Point)
def _(self, coordinates: Point):
left_top_point = self.__get_left_top_local_point()
def _(self, coordinates: Point) -> Point:
left_top_point, _ = self.__get_local_bounds()
return self.__from_ui.point_to_global(coordinates + left_top_point)

@point_to_global.register(RelativelyPoint)
def _(self, coordinates: RelativelyPoint):
x = round(coordinates.x * self.__size.width)
y = round(coordinates.y * self.__size.height)
def _(self, coordinates: RelativelyPoint) -> Point:
left_top_point, right_bottom_point = self.__get_local_bounds()
width = abs(round(right_bottom_point.x - left_top_point.x))
height = abs(round(right_bottom_point.y - left_top_point.y))
x = round(coordinates.x * width)
y = round(coordinates.y * height)
local_point = Point(x, y)
return self.point_to_global(local_point)

Expand All @@ -74,7 +92,7 @@ def point_to_local(self, coordinates: Point) -> Point:
:return: Converted local point.
:rtype: Point
"""
left_top_point = self.__get_left_top_local_point()
left_top_point, _ = self.__get_local_bounds()
return self.__from_ui.point_to_local(coordinates) - left_top_point

def get_screenshot(self) -> numpy.ndarray:
Expand All @@ -84,12 +102,11 @@ def get_screenshot(self) -> numpy.ndarray:
:rtype: numpy.ndarray
"""
screenshot = self.__from_ui.get_screenshot()
left_top_point = self.__get_left_top_local_point()
right_bottom_point = left_top_point + Point(self.__size.width, self.__size.height)

if isinstance(screenshot, numpy.ndarray):
return screenshot[left_top_point.y:right_bottom_point.y, left_top_point.x:right_bottom_point.x]
return screenshot.crop((left_top_point.x, left_top_point.y, right_bottom_point.x, right_bottom_point.y))
left_top_point, right_bottom_point = self.__get_local_bounds()
return screenshot[
left_top_point.y:right_bottom_point.y,
left_top_point.x:right_bottom_point.x,
]

@property
def window(self) -> Window:
Expand Down
Loading
Loading