Skip to content

Latest commit

Β 

History

History
465 lines (357 loc) Β· 20.1 KB

File metadata and controls

465 lines (357 loc) Β· 20.1 KB

Pyrox

Python Version License Development Status Version

Pyrox is a Python back-end engine and application framework built on PyQt6. It provides a rich set of interfaces, models, services, and abstractions for building industrial automation and desktop applications. Pyrox is designed to be used as a foundation β€” downstream projects like ControlRox build their entire application layer on top of it.

πŸš€ Key Features

πŸ—οΈ Interface-Driven Architecture

Pyrox enforces a strict separation of concerns through a dedicated interface layer (pyrox/interfaces/). Every major abstraction β€” applications, tasks, scenes, physics bodies, workspaces, services β€” is defined as a Protocol or ABC in the interface layer before any concrete implementation exists. This allows downstream consumers to depend on contracts, not implementations.

  • Structural Protocols (Protocol + @runtime_checkable): INameable, IDescribable, IRunnable, IConfigurable, IHasId, ISpatial2D, IKinematic2D, IPhysicsBody2D, and more
  • Application Interfaces: IApplication, IApplicationTask, IWorkspace, IViewport
  • Scene Interfaces: IScene, ISceneObject, ISceneGroup, ISceneBridge, ISceneBoundLayer, ICompositeSceneObject
  • Physics Interfaces: IBasePhysicsBody, ICollider2D, IRigidBody2D, IMaterial, IPhysicsEngine
  • Service Interfaces: IEnvironmentManager, ILoggingManager, ILogger

🎨 PyQt6 GUI Framework

Pyrox is built exclusively on PyQt6. All GUI components, the workspace layout, and the application shell are PyQt6-native. There is no tkinter dependency.

  • VSCode-style Workspace: Vertical icon sidebar (QTabWidget), central workspace area for TaskFrame panels, an integrated log window, and a status bar β€” all managed by Workspace
  • TaskFrame System: Custom widgets subclass TaskFrame, expose a .root: QWidget property, and are registered with the workspace for docking and visibility management
  • CommandBar: Composable toolbars built with CommandButton and CommandDropdown widgets
  • AttributeTreeView: Lazy object introspection tree for exploring any Python object at runtime
  • ObjectExplorer: Scene object list with add/remove and selection callbacks
  • PropertyPanel: Auto-generated property editor for objects implementing IHasProperties
  • SceneViewer: Full composited scene viewer including a canvas viewport, object explorer, property panel, scene bridge, and toolbar β€” all wired together
  • YAML/Text Editors: PyroxYamlEditor and TextEditorFrame for in-app editing
  • Dark Theme: DefaultTheme provides consistent color constants across all widgets

🧩 Mixin-Based Service Composition

The ServicesRunnableMixin composes focused sub-mixins to give any class clean, property-based access to all platform services:

Property Service
self.env / self.env_keys EnvManager + EnvironmentKeys
self.gui GuiManager (PyQt6 root, menus, geometry)
self.logging / self.log() LoggingManager
self.directory PlatformDirectoryService
self.root_window / self.file_menu … Convenience accessors on GuiManager

Static service classes (EnvManager, GuiManager, LoggingManager, MenuRegistry, GuiStateService) are never instantiated β€” they expose only class methods and properties.

🏭 Task-Based Application Extension

Applications are extended through ApplicationTask subclasses. Each task:

  1. Is auto-registered with ApplicationTaskFactory via the FactoryTypeABC metaclass β€” no manual wiring required
  2. Is instantiated automatically at application startup via ApplicationTaskFactory.build_tasks(app)
  3. Injects menu commands into the application's menu bar via register_menu_command()
  4. Creates and manages its own TaskFrame panel via create_task_frame() and create_or_raise_frame()
from pyrox.models.task import ApplicationTask
from pyrox.models.gui.frame import TaskFrame

class MyTask(ApplicationTask):
    def __init__(self, application):
        super().__init__(application)
        self.register_menu_command(
            menu=self.file_menu,
            registry_id='my_task.open',
            registry_path='File/My Task',
            index=0,
            label='My Task...',
            command=self.create_or_raise_frame,
        )

    def create_task_frame(self) -> TaskFrame:
        return MyTaskFrame(self.application.workspace.workspace_area)

🎬 Scene & Emulation Environment

Pyrox provides the foundational layer for building 2D scene-based emulation environments:

  • Scene Graph: Scene contains SceneObject, SceneGroup, and CompositeSceneObject instances; all implement interface contracts from pyrox.interfaces
  • Physics Engine: PhysicsEngineService drives a 2D physics simulation. Built-in body types include ConveyorBody, CrateBody, FloorBody, SensorBody, and ActorBody
  • Collision Service: CollisionService with a SpatialGrid for broad-phase collision detection
  • Scene Runner: SceneRunnerService and SceneEventBus manage scene lifecycle and broadcast scene events
  • Scene Viewer Task: SceneviewerApplicationTask wires together the full interactive scene viewer UI
  • Scene Bridge: ISceneBridge / ISceneBoundLayer contracts decouple the rendered canvas layer from the domain scene model

πŸ“‘ Event Bus System

Typed, decoupled communication between components via EventBus:

from pyrox.services.bus import EventBus, Event, EventType

class MyEventType(EventType):
    SOMETHING_HAPPENED = 'something_happened'

class MyEvent(Event[MyEventType]):
    data: str

class MyEventBus(EventBus[MyEventType, MyEvent]):
    pass

MyEventBus.subscribe(MyEventType.SOMETHING_HAPPENED, my_callback)
MyEventBus.publish(MyEvent(event_type=MyEventType.SOMETHING_HAPPENED, data='hello'))

Each EventBus subclass gets its own isolated subscriber registry β€” no cross-bus leakage.

πŸ—οΈ Factory Patterns

MetaFactory enables extensible type registries. FactoryTypeABC auto-registers subclasses with a target factory via __init_subclass__:

from pyrox.models.factory import MetaFactory, FactoryTypeABC

class MyFactory(MetaFactory):
    pass  # gets its own _registered_types dict automatically

class MyBase(FactoryTypeABC[MyFactory]):
    pass  # auto-registers with MyFactory

class MyConcrete(MyBase):
    pass  # auto-registered as 'MyConcrete' in MyFactory._registered_types

πŸ”§ Common Services & Utilities

  • EnvManager: .env-based configuration via python-dotenv; typed get() with defaults
  • GuiManager: Static PyQt6 root window, menu bar, and geometry management; event scheduling
  • LoggingManager: Named loggers, stream capture, and callback routing for log output
  • PlatformDirectoryService: Cross-platform user data, log, and config directories via platformdirs
  • GuiStateService: Persists GUI layout preferences (splitter ratios, sidebar widths, etc.) as JSON between sessions
  • MenuRegistry: Centralized registry for all menu items; supports enable_item() and set_command() from anywhere
  • SceneRunnerService / SceneEventBus: Scene lifecycle management and event broadcasting
  • CollisionService / SpatialGrid: 2D spatial indexing and collision detection
  • PhysicsEngineService: 2D physics simulation driver
  • StatusUpdateEventBus: Application-wide status bar messaging

πŸ“¦ Installation

Requirements

  • Python 3.13.9+ (Required)
  • Cross-platform support (Windows, Linux, macOS)

Quick Install

# Clone the repository
git clone https://github.com/iroxusux/Pyrox.git
cd Pyrox

# Run the installation script (recommended β€” handles venv, deps, and git hooks)
./install.sh

# Or install manually:
pip install -e . --upgrade
python utils/setup_hooks.py  # Set up pre-commit hooks

Dependencies

Key runtime dependencies installed automatically:

Package Purpose
PyQt6 GUI framework (required)
python-dotenv .env configuration loading
platformdirs Cross-platform directory resolution
pyyaml YAML file support
lxml XML processing
pandas / openpyxl Data and Excel processing
Pillow Image processing
pdfplumber / PyMuPDF PDF processing
pylogix Allen-Bradley PLC communication
tomli TOML parsing

🏁 Quick Start

Minimal Application

# myapp/__main__.py
import pyrox
from pyrox.application import Application

app = Application()
app.run()

Configuration is read from .env at startup:

# .env
APP_NAME=My Application
APP_DESCRIPTION=Built on Pyrox
APP_AUTHOR=Your Name

Adding a Custom Task

from pyrox.models.task import ApplicationTask
from pyrox.models.gui.frame import TaskFrame
from pyrox.interfaces import IApplication
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget

class MyTaskFrame(TaskFrame):
    def __init__(self, parent: QWidget):
        super().__init__()
        layout = QVBoxLayout()
        layout.addWidget(QLabel("Hello from MyTask!"))
        self._root = QWidget(parent)
        self._root.setLayout(layout)

    @property
    def root(self) -> QWidget:
        return self._root


class MyTask(ApplicationTask):
    def __init__(self, application: IApplication) -> None:
        super().__init__(application)
        self.register_menu_command(
            menu=self.file_menu,
            registry_id='my_task.open',
            registry_path='File/My Task',
            index=0,
            label='My Task...',
            command=self.create_or_raise_frame,
        )

    def create_task_frame(self) -> TaskFrame:
        return MyTaskFrame(self.application.workspace.workspace_area)

MyTask is automatically discovered and built at startup β€” no registration call needed.

Using the Logging Service

from pyrox.services.logging import log

class MyClass:
    def do_work(self):
        log(self).info('Starting work...')
        log(self).debug('Detail message')

Publishing a Status Update

from pyrox.services.status import StatusUpdateEventBus, StatusUpdateEvent, StatusUpdateEventType

StatusUpdateEventBus.publish(
    StatusUpdateEvent(
        event_type=StatusUpdateEventType.UPDATE,
        status_message='Processing complete'
    )
)

πŸ—οΈ Architecture

Package Layout

pyrox/
β”œβ”€β”€ interfaces/           # Abstract contracts only β€” no implementations
β”‚   β”œβ”€β”€ protocols/        # Structural Protocol definitions
β”‚   β”‚   β”œβ”€β”€ meta.py       # INameable, IRunnable, IConfigurable, IHasId, …
β”‚   β”‚   β”œβ”€β”€ coord.py      # ICoord2D, IArea2D
β”‚   β”‚   β”œβ”€β”€ spatial.py    # ISpatial2D, IRotatable, IDirectional2D, IZoomable
β”‚   β”‚   β”œβ”€β”€ kinematic.py  # IVelocity2D, IKinematic2D
β”‚   β”‚   β”œβ”€β”€ physics.py    # IPhysicsBody2D, IRigidBody2D, IMaterial, ICollider2D
β”‚   β”‚   β”œβ”€β”€ property.py   # IHasProperties
β”‚   β”‚   β”œβ”€β”€ connection.py # IConnectable, Connection
β”‚   β”‚   └── gui.py        # IHasCanvas
β”‚   β”œβ”€β”€ scene/            # IScene, ISceneObject, ISceneGroup, ISceneBridge, …
β”‚   β”œβ”€β”€ gui/              # IWorkspace, IViewport
β”‚   β”œβ”€β”€ physics.py        # IBasePhysicsBody
β”‚   β”œβ”€β”€ application.py    # IApplication, IApplicationTask
β”‚   β”œβ”€β”€ services.py       # IEnvironmentManager, ILoggingManager, ILogger
β”‚   └── constants.py      # EnvironmentKeys
β”‚
β”œβ”€β”€ models/               # Concrete implementations
β”‚   β”œβ”€β”€ factory.py        # MetaFactory, FactoryTypeABC
β”‚   β”œβ”€β”€ task.py           # ApplicationTask, ApplicationTaskFactory
β”‚   β”œβ”€β”€ services.py       # ServicesRunnableMixin (Supports* sub-mixins)
β”‚   β”œβ”€β”€ meta.py           # PyroxObject, SnowFlake, SliceableInt
β”‚   β”œβ”€β”€ list.py           # HashList, SafeList, TrackedList, Subscribable
β”‚   β”œβ”€β”€ runtime.py        # RuntimeDict
β”‚   β”œβ”€β”€ protocols/        # CoreMixin, Nameable, HasId, PhysicsBody2D, …
β”‚   β”œβ”€β”€ scene/            # Scene, SceneObject, SceneGroup, SceneBridge, …
β”‚   β”œβ”€β”€ physics/          # BasePhysicsBody, ConveyorBody, CrateBody, FloorBody, …
β”‚   └── gui/              # All PyQt6 widgets
β”‚       β”œβ”€β”€ frame.py      # TaskFrame base class
β”‚       β”œβ”€β”€ workspace.py  # Workspace (VSCode-style layout)
β”‚       β”œβ”€β”€ commandbar.py # CommandBar, CommandButton, CommandDropdown
β”‚       β”œβ”€β”€ treeview.py   # AttributeTreeView
β”‚       β”œβ”€β”€ objectexplorer.py   # ObjectExplorer
β”‚       β”œβ”€β”€ propertypanel.py    # PropertyPanel
β”‚       β”œβ”€β”€ logframe.py         # LogFrame
β”‚       β”œβ”€β”€ theme.py            # DefaultTheme
β”‚       β”œβ”€β”€ yamleditor.py       # PyroxYamlEditor
β”‚       β”œβ”€β”€ texteditor.py       # TextEditorFrame
β”‚       β”œβ”€β”€ contextmenu.py      # PyroxContextMenu
β”‚       └── sceneviewer/        # Full scene viewer composite widget
β”‚
β”œβ”€β”€ services/             # Static-class utilities
β”‚   β”œβ”€β”€ logging.py        # log(), LoggingManager, StreamCapture
β”‚   β”œβ”€β”€ env.py            # EnvManager
β”‚   β”œβ”€β”€ gui.py            # GuiManager (PyQt6)
β”‚   β”œβ”€β”€ gui_state.py      # GuiStateService
β”‚   β”œβ”€β”€ menu_registry.py  # MenuRegistry, MenuItemDescriptor
β”‚   β”œβ”€β”€ bus.py            # EventBus, Event, EventType
β”‚   β”œβ”€β”€ status.py         # StatusUpdateEventBus
β”‚   β”œβ”€β”€ scene.py          # SceneRunnerService, SceneEventBus, HasSceneMixin
β”‚   β”œβ”€β”€ file.py           # get_open_file, get_save_file, PlatformDirectoryService
β”‚   β”œβ”€β”€ theme.py          # ThemeManager
β”‚   β”œβ”€β”€ collision.py      # CollisionService, SpatialGrid
β”‚   └── physics.py        # PhysicsEngineService
β”‚
β”œβ”€β”€ tasks/                # Built-in ApplicationTask implementations
β”‚   β”œβ”€β”€ builtin.py        # FileTask, HelpTask, ToolsTask, ViewTask
β”‚   └── sceneviewer.py    # SceneviewerApplicationTask
β”‚
β”œβ”€β”€ application.py        # Application (concrete IApplication entry point)
└── ui/                   # Icons and splash assets

Architectural Layers

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Downstream Application                 β”‚
β”‚         (e.g. ControlRox, custom apps)              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                pyrox/tasks/                         β”‚
β”‚        ApplicationTask subclasses (built-in)        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  pyrox/models/     β”‚  pyrox/services/               β”‚
β”‚  Concrete impls    β”‚  Static service classes         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                pyrox/interfaces/                    β”‚
β”‚     Protocols, ABCs β€” no implementation code        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Core Design Patterns

Pattern Where Used
Interface Segregation pyrox/interfaces/ β€” one interface per concern
Mixin Composition ServicesRunnableMixin composed of Supports* mixins
Factory + Auto-registration MetaFactory + FactoryTypeABC.__init_subclass__
Event Bus EventBus[T, E] with per-subclass subscriber isolation
RuntimeDict Dict with save/change callbacks for persistent state
Menu Registry MenuRegistry centralises all menu items across tasks

🎯 Use Cases

Pyrox as a Back-End Engine

Pyrox provides the scaffolding β€” interface contracts, service infrastructure, GUI shell, scene engine, and physics layer β€” so that application developers can focus exclusively on domain logic.

  • ControlRox: Industrial automation application built directly on Pyrox; uses ApplicationTask, Scene, SceneObject, and the PyQt6 workspace
  • Custom desktop tools: Any Python desktop application that needs a structured, extensible PyQt6 shell
  • Emulation environments: Applications that simulate physical processes using Pyrox's scene graph and physics engine

Building on Pyrox

  1. New application: Subclass Application from pyrox.application
  2. New feature/panel: Subclass ApplicationTask; implement create_task_frame() and register menu commands in __init__
  3. New GUI widget: Subclass TaskFrame; expose .root: QWidget for workspace integration
  4. New service: Add a static class to pyrox/services/; expose via pyrox/services/__init__.py
  5. New interface: Add a Protocol or ABC to pyrox/interfaces/ with no implementation
  6. New event: Subclass EventType, Event, and EventBus β€” one class per domain
  7. New scene object type: Subclass SceneObject and implement the ISceneObject interface
  8. New physics body: Subclass BasePhysicsBody and register with PhysicsSceneFactory

πŸ› οΈ Development

Setup

# Install with venv and git hooks (recommended)
./install.sh

# Or manually
pip install -e . --upgrade
python utils/setup_hooks.py

Running Tests

pytest pyrox/services/test/
pytest pyrox/models/test/
pytest pyrox/tasks/test/

Building Distribution

./build.sh

Utility Scripts

Script Purpose
utils/sync_readme.py Sync README version/status badges from pyproject.toml
utils/check_version_increment.py Verify a version bump exists for code changes
utils/setup_hooks.py Install pre-commit hooks
utils/extract_todos.py Generate code_todos.md from source TODOs

Pre-commit Hooks

The pre-commit hook automatically:

  1. Checks for a version bump when Python source files change
  2. Syncs README badges from pyproject.toml (version, requires-python, classifiers)

Files that do not require a version bump: README.md, .md files, docs/, .gitignore, LICENSE, .yml/.yaml, utils/, hooks/

Files that do require a version bump: pyrox/**/*.py, pyproject.toml

🏭 Related Projects

  • ControlRox β€” Industrial automation application built on Pyrox

🀝 Contributing

Contributions are welcome! Please maintain architectural consistency:

  • New interfaces go in pyrox/interfaces/ with no implementation code
  • New concrete implementations go in pyrox/models/
  • New services go in pyrox/services/ as static classes
  • All code must target Python 3.13.9+ and use PyQt6 for any GUI work
  • Use built-in generic types (list[str], dict[str, int]) β€” never typing.List, typing.Dict, etc.
  • Use X | Y unions β€” never Optional[X] or Union[X, Y]
  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

πŸ“„ License

This project is licensed under the GNU General Public License v3.0 β€” see the LICENSE file for details.

πŸ‘¨β€πŸ’» Author

Brian LaFond
πŸ“§ Brian.L.LaFond@gmail.com
πŸ™ GitHub


Pyrox β€” PyQt6-based back-end engine providing interfaces, models, services, and a scene/emulation environment for building industrial automation and desktop applications