Repository navigation
Core Architecture
The architecture of osn-selenium is built on the principle of strict decoupling. By separating data models, abstract interfaces, and execution logic, the framework achieves high maintainability and functional parity between its synchronous and asynchronous versions.
At the lowest level, the framework uses a standardized set of models to represent data, geometry, and configuration. Most models inherit from Pydantic-based base classes found in _base_models.py to ensure strict validation.
-
DictModel: A strict Pydantic model that forbids extra fields, enforces type validation, and strips whitespace from strings. It serves as the base for most configuration objects. -
ExtraDictModel: A flexible version ofDictModelthat allows additional fields, used primarily for browser flags and experimental options where future-proofing is necessary.
The framework provides several lightweight models to standardize browser coordinates and dimensions:
-
Point: Represents a 2D point with integer$(x, y)$ coordinates. -
Position: ADictModelrepresentation of a coordinate. -
Size: Represents dimensions (width, height). -
Rectangle: CombinesPositionandSizeto define element bounds. -
WindowRect: A specialized model for browser window geometry with sensible defaults (e.g.,$1024 \times 768$ ).
To solve the "God Object" problem found in standard Selenium, osn-selenium defines every component through Abstract Base Classes (ABCs).
The abstract/ directory contains the "contracts" that all implementations must follow. This ensures that whether you are using a sync Chrome driver or a trio_threads Edge driver, the method names, arguments, and expected return types are identical.
| Abstract Category | Purpose |
|---|---|
executors/ |
Defines how CDP and JavaScript commands should be routed. |
instances/ |
Defines the interface for objects like WebElement, Alert, and ShadowRoot. |
webdriver/ |
Defines the core lifecycle and navigation methods for the driver itself. |
The TrioThreadMixin (found in osn_selenium/trio_threads_mixin.py) is the core utility that enables the asynchronous trio_threads architecture.
Since the underlying Selenium/WebDriver protocol is inherently blocking (synchronous), this mixin provides a bridge to the Trio event loop.
-
sync_to_trio: A decorator/wrapper that takes a blocking synchronous function and executes it in an external worker thread usingtrio.to_thread.run_sync. It also handles ContextVariable exchange, ensuring that variables likeCURRENT_BROWSING_CONTEXTare preserved across worker threads.
# A standard blocking function
def sync_foo(name: str, value: int):
# Imagine a slow or blocking operation here
return f"Hello {name}, your ID is {value}"
# Inside a class inheriting from TrioThreadMixin:
async def trio_threads_demo(self):
# Wrap the function to make it async
async_foo = self.sync_to_trio(sync_foo, lock=True)
# Now you can await it without blocking the event loop
result = await async_foo("User", value=42)
print(result) # Output: Hello User, your ID is 42-
Context Management:
sync_to_trio_contextallows synchronous context managers to be used as asynchronous ones.
from contextlib import contextmanager
# A standard synchronous context manager factory
@contextmanager
def sync_bar_resource(resource_id: str):
print(f"Connecting to {resource_id}...")
yield f"Resource-Object-{resource_id}"
print(f"Disconnecting from {resource_id}...")
# Inside a class inheriting from TrioThreadMixin:
async def trio_threads_context_demo(self):
# Wrap the factory function
async_bar_factory = self.sync_to_trio_context(sync_bar_resource, lock=True)
# Now use it with 'async with'
# Both entering and exiting will happen in worker threads
async with async_bar_factory("API_01") as resource:
print(f"Using: {resource}")-
Concurrency Control: It manages a
trio.CapacityLimiterto limit the number of concurrent threads and atrio.Lockto ensure thread-safe access to the driver instance.
The framework is split into two primary architectures that determine how you interact with the library.
- Execution: Methods are called directly and block the execution of the script until the browser responds.
- Logic: Directly implements the abstract interfaces using standard blocking calls.
-
Execution: Every method is a coroutine and must be called using
await. -
Logic: Uses the
TrioThreadMixinto offload blocking driver calls to threads. This allows a single Python script to manage dozens of browser instances simultaneously without freezing the event loop.
- Execution: Fully asynchronous, coroutine-based.
- Logic: Utilizes the BiDiBridge to translate W3C commands into direct WebSocket messages. Moves away from thread pools for command execution, offering higher performance and context isolation.
Despite the different execution models, the implementations are functionally identical.
# Synchronous Interaction
driver.get("https://example.com")
el = driver.find_element(By.ID, "login")
# Asynchronous Interaction (TrioThreads & BiDi)
await driver.get("https://example.com")
el = await driver.find_element(By.ID, "login")This consistency is achieved by having both implementations wrap the same unified logic, ensuring that transitioning from sync to async requires only changing the imports and adding await keywords.