From 4edc45b2e81750e79786389c6bb417bc616cea62 Mon Sep 17 00:00:00 2001 From: Cristian Pufu Date: Sun, 25 Jan 2026 12:16:12 +0200 Subject: [PATCH] fix: extend factory protocol --- README.md | 6 ---- pyproject.toml | 4 +-- src/uipath/runtime/__init__.py | 8 ++--- src/uipath/runtime/factory.py | 43 ++++++++++++++--------- src/uipath/runtime/resumable/protocols.py | 35 ++---------------- src/uipath/runtime/storage.py | 42 ++++++++++++++++++++++ tests/test_factory.py | 37 ++++++++++++++++--- tests/test_registry.py | 36 +++++++++++++++---- uv.lock | 34 +++++++++--------- 9 files changed, 156 insertions(+), 89 deletions(-) create mode 100644 src/uipath/runtime/storage.py diff --git a/README.md b/README.md index f82181a..9a1b034 100644 --- a/README.md +++ b/README.md @@ -169,9 +169,6 @@ class MyRuntimeFactory: async def new_runtime(self, entrypoint: str, runtime_id: str) -> UiPathRuntimeProtocol: return MyRuntime() - async def discover_runtimes(self) -> list[UiPathRuntimeProtocol]: - return [] - def discover_entrypoints(self) -> list[str]: return [] @@ -315,9 +312,6 @@ class ChildRuntimeFactory: async def new_runtime(self, entrypoint: str) -> UiPathRuntimeProtocol: return ChildRuntime(name=entrypoint) - async def discover_runtimes(self) -> list[UiPathRuntimeProtocol]: - return [] - def discover_entrypoints(self) -> list[str]: return [] diff --git a/pyproject.toml b/pyproject.toml index df76aff..2b562f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "uipath-runtime" -version = "0.5.1" +version = "0.6.0" description = "Runtime abstractions and interfaces for building agents and automation scripts in the UiPath ecosystem" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" dependencies = [ - "uipath-core>=0.1.1, <0.2.0", + "uipath-core>=0.2.0, <0.3.0", ] classifiers = [ "Intended Audience :: Developers", diff --git a/src/uipath/runtime/__init__.py b/src/uipath/runtime/__init__.py index 7932492..e088dfd 100644 --- a/src/uipath/runtime/__init__.py +++ b/src/uipath/runtime/__init__.py @@ -18,9 +18,8 @@ ) from uipath.runtime.events import UiPathRuntimeEvent from uipath.runtime.factory import ( - UiPathRuntimeCreatorProtocol, UiPathRuntimeFactoryProtocol, - UiPathRuntimeScannerProtocol, + UiPathRuntimeFactorySettings, ) from uipath.runtime.registry import UiPathRuntimeFactoryRegistry from uipath.runtime.result import ( @@ -41,6 +40,7 @@ UiPathResumeTriggerType, ) from uipath.runtime.schema import UiPathRuntimeSchema +from uipath.runtime.storage import UiPathRuntimeStorageProtocol __all__ = [ "UiPathExecuteOptions", @@ -48,10 +48,10 @@ "UiPathRuntimeContext", "UiPathRuntimeProtocol", "UiPathExecutionRuntime", - "UiPathRuntimeCreatorProtocol", - "UiPathRuntimeScannerProtocol", + "UiPathRuntimeStorageProtocol", "UiPathRuntimeFactoryProtocol", "UiPathRuntimeFactoryRegistry", + "UiPathRuntimeFactorySettings", "UiPathRuntimeResult", "UiPathRuntimeStatus", "UiPathRuntimeEvent", diff --git a/src/uipath/runtime/factory.py b/src/uipath/runtime/factory.py index 3f003ec..c9481bd 100644 --- a/src/uipath/runtime/factory.py +++ b/src/uipath/runtime/factory.py @@ -2,35 +2,44 @@ from typing import Protocol -from uipath.runtime.base import UiPathDisposableProtocol, UiPathRuntimeProtocol +from pydantic import BaseModel +from uipath.core.tracing import UiPathTraceSettings +from uipath.runtime.base import ( + UiPathDisposableProtocol, + UiPathRuntimeProtocol, +) +from uipath.runtime.storage import UiPathRuntimeStorageProtocol -class UiPathRuntimeScannerProtocol(Protocol): - """Protocol for discovering all UiPath runtime instances.""" - async def discover_runtimes(self) -> list[UiPathRuntimeProtocol]: - """Discover all runtime classes.""" - ... +class UiPathRuntimeFactorySettings(BaseModel): + """Runtime settings for execution behavior.""" + + model_config = {"arbitrary_types_allowed": True} # Needed for Callable + + trace_settings: UiPathTraceSettings | None = None + + +class UiPathRuntimeFactoryProtocol( + UiPathDisposableProtocol, + Protocol, +): + """Protocol for discovering and creating UiPath runtime instances.""" def discover_entrypoints(self) -> list[str]: """Discover all runtime entrypoints.""" ... - -class UiPathRuntimeCreatorProtocol(Protocol): - """Protocol for creating a UiPath runtime given an entrypoint.""" - async def new_runtime( self, entrypoint: str, runtime_id: str, **kwargs ) -> UiPathRuntimeProtocol: """Create a new runtime instance.""" ... + async def get_storage(self) -> UiPathRuntimeStorageProtocol | None: + """Get the factory storage.""" + ... -class UiPathRuntimeFactoryProtocol( - UiPathRuntimeCreatorProtocol, - UiPathRuntimeScannerProtocol, - UiPathDisposableProtocol, - Protocol, -): - """Protocol for discovering and creating UiPath runtime instances.""" + async def get_settings(self) -> UiPathRuntimeFactorySettings | None: + """Get factory settings.""" + ... diff --git a/src/uipath/runtime/resumable/protocols.py b/src/uipath/runtime/resumable/protocols.py index 1396bb3..bef5750 100644 --- a/src/uipath/runtime/resumable/protocols.py +++ b/src/uipath/runtime/resumable/protocols.py @@ -3,9 +3,10 @@ from typing import Any, Protocol from uipath.runtime.resumable.trigger import UiPathResumeTrigger +from uipath.runtime.storage import UiPathRuntimeStorageProtocol -class UiPathResumableStorageProtocol(Protocol): +class UiPathResumableStorageProtocol(UiPathRuntimeStorageProtocol, Protocol): """Protocol for storing and retrieving resume triggers.""" async def save_triggers( @@ -46,38 +47,6 @@ async def delete_trigger( """ ... - async def set_value( - self, runtime_id: str, namespace: str, key: str, value: Any - ) -> None: - """Store values for a specific runtime. - - Args: - runtime_id: The runtime ID - namespace: The namespace of the persisted value - key: The key associated with the persisted value - value: The value to persist - - Raises: - Exception: If storage operation fails - """ - ... - - async def get_value(self, runtime_id: str, namespace: str, key: str) -> Any: - """Retrieve values for a specific runtime from storage. - - Args: - runtime_id: The runtime ID - namespace: The namespace of the persisted value - key: The key associated with the persisted value - - Returns: - The value matching the method's parameters, or None if it does not exist - - Raises: - Exception: If retrieval operation fails - """ - ... - class UiPathResumeTriggerCreatorProtocol(Protocol): """Protocol for creating resume triggers from suspend values.""" diff --git a/src/uipath/runtime/storage.py b/src/uipath/runtime/storage.py new file mode 100644 index 0000000..3a18b09 --- /dev/null +++ b/src/uipath/runtime/storage.py @@ -0,0 +1,42 @@ +"""Runtime storage protocol definition.""" + +from typing import ( + Any, + Protocol, +) + + +class UiPathRuntimeStorageProtocol(Protocol): + """Protocol for runtime storage operations.""" + + async def set_value( + self, runtime_id: str, namespace: str, key: str, value: Any + ) -> None: + """Store values for a specific runtime. + + Args: + runtime_id: The runtime ID + namespace: The namespace of the persisted value + key: The key associated with the persisted value + value: The value to persist + + Raises: + Exception: If storage operation fails + """ + ... + + async def get_value(self, runtime_id: str, namespace: str, key: str) -> Any: + """Retrieve values for a specific runtime from storage. + + Args: + runtime_id: The runtime ID + namespace: The namespace of the persisted value + key: The key associated with the persisted value + + Returns: + The value matching the method's parameters, or None if it does not exist + + Raises: + Exception: If retrieval operation fails + """ + ... diff --git a/tests/test_factory.py b/tests/test_factory.py index a1a61b3..31916af 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -8,12 +8,29 @@ UiPathRuntimeProtocol, UiPathRuntimeResult, UiPathRuntimeSchema, + UiPathRuntimeStorageProtocol, UiPathStreamOptions, ) -from uipath.runtime.factory import UiPathRuntimeCreatorProtocol +from uipath.runtime.factory import ( + UiPathRuntimeFactoryProtocol, + UiPathRuntimeFactorySettings, +) + + +class MockStorage(UiPathRuntimeStorageProtocol): + """Mock storage implementation""" + + def __init__(self): + self._store = {} + + async def set_value(self, runtime_id, namespace, key, value): + self._store.setdefault(runtime_id, {}).setdefault(namespace, {})[key] = value + + async def get_value(self, runtime_id, namespace, key): + return self._store.get(runtime_id, {}).get(namespace, {}).get(key) -class MockRuntime: +class MockRuntime(UiPathRuntimeProtocol): """Mock runtime that implements UiPathRuntimeProtocol.""" def __init__(self, settings: dict[str, Any] | None = None) -> None: @@ -49,16 +66,28 @@ async def dispose(self) -> None: class CreatorWithKwargs: """Implementation with kwargs.""" + def discover_entrypoints(self) -> list[str]: + return ["main.py"] + async def new_runtime( self, entrypoint: str, runtime_id: str, **kwargs ) -> UiPathRuntimeProtocol: return MockRuntime(kwargs.get("settings")) + async def get_storage(self) -> UiPathRuntimeStorageProtocol | None: + return MockStorage() + + async def get_settings(self) -> UiPathRuntimeFactorySettings | None: + return UiPathRuntimeFactorySettings() + + async def dispose(self) -> None: + pass + @pytest.mark.asyncio async def test_protocol_works_with_kwargs_not_specified(): """Test protocol works with implementation that has kwargs.""" - creator: UiPathRuntimeCreatorProtocol = CreatorWithKwargs() + creator: UiPathRuntimeFactoryProtocol = CreatorWithKwargs() runtime = await creator.new_runtime("main.py", "runtime-123") assert isinstance(runtime, MockRuntime) @@ -66,7 +95,7 @@ async def test_protocol_works_with_kwargs_not_specified(): @pytest.mark.asyncio async def test_protocol_works_with_kwargs_specified(): """Test protocol works with implementation that has kwargs.""" - creator: UiPathRuntimeCreatorProtocol = CreatorWithKwargs() + creator: UiPathRuntimeFactoryProtocol = CreatorWithKwargs() runtime = await creator.new_runtime( "main.py", "runtime-123", settings={"timeout": 30, "model": "gpt-4"} ) diff --git a/tests/test_registry.py b/tests/test_registry.py index 9d1bb75..86eda5b 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -10,14 +10,29 @@ UiPathRuntimeEvent, UiPathRuntimeFactoryProtocol, UiPathRuntimeFactoryRegistry, + UiPathRuntimeFactorySettings, UiPathRuntimeProtocol, UiPathRuntimeResult, UiPathRuntimeSchema, UiPathRuntimeStatus, + UiPathRuntimeStorageProtocol, UiPathStreamOptions, ) +class MockStorage(UiPathRuntimeStorageProtocol): + """Mock storage implementation""" + + def __init__(self): + self._store = {} + + async def set_value(self, runtime_id, namespace, key, value): + self._store.setdefault(runtime_id, {}).setdefault(namespace, {})[key] = value + + async def get_value(self, runtime_id, namespace, key): + return self._store.get(runtime_id, {}).get(namespace, {}).get(key) + + class MockRuntime(UiPathRuntimeProtocol): """Mock runtime instance""" @@ -59,8 +74,11 @@ def __init__(self, context: Optional[UiPathRuntimeContext] = None): def discover_entrypoints(self) -> list[str]: return ["main.py", "handler.py"] - async def discover_runtimes(self) -> list[UiPathRuntimeProtocol]: - return [] + async def get_storage(self) -> UiPathRuntimeStorageProtocol | None: + return MockStorage() + + async def get_settings(self) -> UiPathRuntimeFactorySettings | None: + return UiPathRuntimeFactorySettings() async def new_runtime( self, entrypoint: str, runtime_id: str, **kwargs @@ -81,8 +99,11 @@ def __init__(self, context: Optional[UiPathRuntimeContext] = None): def discover_entrypoints(self) -> list[str]: return ["agent", "workflow"] - async def discover_runtimes(self) -> list[UiPathRuntimeProtocol]: - return [] + async def get_storage(self) -> UiPathRuntimeStorageProtocol | None: + return MockStorage() + + async def get_settings(self) -> UiPathRuntimeFactorySettings | None: + return UiPathRuntimeFactorySettings() async def new_runtime( self, entrypoint: str, runtime_id: str, **kwargs @@ -103,8 +124,11 @@ def __init__(self, context: Optional[UiPathRuntimeContext] = None): def discover_entrypoints(self) -> list[str]: return ["chatbot", "rag"] - async def discover_runtimes(self) -> list[UiPathRuntimeProtocol]: - return [] + async def get_storage(self) -> UiPathRuntimeStorageProtocol | None: + return MockStorage() + + async def get_settings(self) -> UiPathRuntimeFactorySettings | None: + return UiPathRuntimeFactorySettings() async def new_runtime( self, entrypoint: str, runtime_id: str, **kwargs diff --git a/uv.lock b/uv.lock index 70ef259..5ad2469 100644 --- a/uv.lock +++ b/uv.lock @@ -430,20 +430,20 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.39.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/0b/e5428c009d4d9af0515b0a8371a8aaae695371af291f45e702f7969dce6b/opentelemetry_api-1.39.0.tar.gz", hash = "sha256:6130644268c5ac6bdffaf660ce878f10906b3e789f7e2daa5e169b047a2933b9", size = 65763, upload-time = "2025-12-03T13:19:56.378Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/85/d831a9bc0a9e0e1a304ff3d12c1489a5fbc9bf6690a15dcbdae372bbca45/opentelemetry_api-1.39.0-py3-none-any.whl", hash = "sha256:3c3b3ca5c5687b1b5b37e5c5027ff68eacea8675241b29f13110a8ffbb8f0459", size = 66357, upload-time = "2025-12-03T13:19:33.043Z" }, + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.60b0" +version = "0.60b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -451,36 +451,36 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/3c/bd53dbb42eff93d18e3047c7be11224aa9966ce98ac4cc5bfb860a32c95a/opentelemetry_instrumentation-0.60b0.tar.gz", hash = "sha256:4e9fec930f283a2677a2217754b40aaf9ef76edae40499c165bc7f1d15366a74", size = 31707, upload-time = "2025-12-03T13:22:00.352Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/7b/5b5b9f8cfe727a28553acf9cd287b1d7f706f5c0a00d6e482df55b169483/opentelemetry_instrumentation-0.60b0-py3-none-any.whl", hash = "sha256:aaafa1483543a402819f1bdfb06af721c87d60dd109501f9997332862a35c76a", size = 33096, upload-time = "2025-12-03T13:20:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.39.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/e3/7cd989003e7cde72e0becfe830abff0df55c69d237ee7961a541e0167833/opentelemetry_sdk-1.39.0.tar.gz", hash = "sha256:c22204f12a0529e07aa4d985f1bca9d6b0e7b29fe7f03e923548ae52e0e15dde", size = 171322, upload-time = "2025-12-03T13:20:09.651Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/b4/2adc8bc83eb1055ecb592708efb6f0c520cc2eb68970b02b0f6ecda149cf/opentelemetry_sdk-1.39.0-py3-none-any.whl", hash = "sha256:90cfb07600dfc0d2de26120cebc0c8f27e69bf77cd80ef96645232372709a514", size = 132413, upload-time = "2025-12-03T13:19:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.60b0" +version = "0.60b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/0e/176a7844fe4e3cb5de604212094dffaed4e18b32f1c56b5258bcbcba85c2/opentelemetry_semantic_conventions-0.60b0.tar.gz", hash = "sha256:227d7aa73cbb8a2e418029d6b6465553aa01cf7e78ec9d0bc3255c7b3ac5bf8f", size = 137935, upload-time = "2025-12-03T13:20:12.395Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/56/af0306666f91bae47db14d620775604688361f0f76a872e0005277311131/opentelemetry_semantic_conventions-0.60b0-py3-none-any.whl", hash = "sha256:069530852691136018087b52688857d97bba61cd641d0f8628d2d92788c4f78a", size = 219981, upload-time = "2025-12-03T13:19:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, ] [[package]] @@ -991,21 +991,21 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.1.1" +version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/74/0aa4d000bb545936a23e5afaeb6a6ec7040973dafcc595c33e458c867a05/uipath_core-0.1.1.tar.gz", hash = "sha256:c02f742619b8491a5e31138cb9955dd1b4b97c06fd3b8e797bc14cb3754abce6", size = 88414, upload-time = "2025-12-09T12:47:00.206Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/4f/9bf150a21b6af8b56edf7fbca46827806570eab5b37f90c2b76180cf1e79/uipath_core-0.2.0.tar.gz", hash = "sha256:950427fe7921a67468416856faf63192cf717d8adce092d706b070c487f0c076", size = 103072, upload-time = "2026-01-25T11:59:10.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/7c/2a3e77cbaaf36be3193c4d7578fff5fdb6855a18e34aa9a5de33f6ccd0a2/uipath_core-0.1.1-py3-none-any.whl", hash = "sha256:9ea17343604c4cfc4427d637ecc82752fb39865c8df9ab255b446a5cabff1d0d", size = 23954, upload-time = "2025-12-09T12:46:58.772Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/f61f6aace058d61dfa11e3c2116b06f0bc15c45d9d201bf432902f54018f/uipath_core-0.2.0-py3-none-any.whl", hash = "sha256:bb5366bfca7ec4611f91a0035df194a56eef11f447313491557e131e6090f5e6", size = 32826, upload-time = "2026-01-25T11:59:09.203Z" }, ] [[package]] name = "uipath-runtime" -version = "0.5.1" +version = "0.6.0" source = { editable = "." } dependencies = [ { name = "uipath-core" }, @@ -1027,7 +1027,7 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "uipath-core", specifier = ">=0.1.1,<0.2.0" }] +requires-dist = [{ name = "uipath-core", specifier = ">=0.2.0,<0.3.0" }] [package.metadata.requires-dev] dev = [