uipath 2.0.12__tar.gz → 2.0.14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of uipath might be problematic. Click here for more details.
- {uipath-2.0.12 → uipath-2.0.14}/PKG-INFO +1 -1
- {uipath-2.0.12 → uipath-2.0.14}/pyproject.toml +1 -1
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_run.py +2 -2
- uipath-2.0.14/src/uipath/tracing/__init__.py +3 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/tracing/_otel_exporters.py +32 -12
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/tracing/_traced.py +116 -38
- uipath-2.0.14/tests/tracing/test_otel_exporters.py +190 -0
- uipath-2.0.14/tests/tracing/test_tracing_manager.py +165 -0
- {uipath-2.0.12 → uipath-2.0.14}/uv.lock +1509 -1509
- uipath-2.0.12/src/uipath/tracing/__init__.py +0 -3
- {uipath-2.0.12 → uipath-2.0.14}/.cursorrules +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/.editorconfig +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/.gitattributes +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/.github/workflows/build.yml +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/.github/workflows/cd.yml +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/.github/workflows/ci.yml +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/.github/workflows/commitlint.yml +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/.github/workflows/lint.yml +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/.github/workflows/test.yml +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/.gitignore +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/.pre-commit-config.yaml +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/.python-version +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/.vscode/extensions.json +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/.vscode/settings.json +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/CONTRIBUTING.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/LICENSE +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/README.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/CONTRIBUTING.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/actions.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/assets/uipath-logo.svg +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/assets.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/buckets.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/connections.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/context_grounding.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/getting_started_agent.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/getting_started_cli.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/getting_started_cloud.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/getting_started_sdk.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/index.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/jobs.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/processes.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/queues.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/docs/sdk.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/justfile +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/mkdocs.yml +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/py.typed +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/__init__.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/README.md +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/__init__.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/_auth_server.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/_models.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/_oidc_utils.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/_portal_service.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/_utils.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/auth_config.json +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/index.html +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/localhost.crt +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/localhost.key +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_runtime/_contracts.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_runtime/_logging.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_runtime/_runtime.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_templates/.psmdcp.template +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_templates/.rels.template +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_templates/[Content_Types].xml.template +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_templates/main.py.template +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_templates/package.nuspec.template +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_utils/_common.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_utils/_input_args.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_utils/_parse_ast.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_auth.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_deploy.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_init.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_new.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_pack.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_publish.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/middlewares.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_config.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_execution_context.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_folder_context.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/__init__.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/_base_service.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/actions_service.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/api_client.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/assets_service.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/buckets_service.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/connections_service.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/connections_service.pyi +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/context_grounding_service.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/folder_service.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/jobs_service.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/llm_gateway_service.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/processes_service.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/queues_service.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_uipath.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/__init__.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/_endpoint.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/_infer_bindings.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/_logs.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/_request_override.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/_request_spec.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/_user_agent.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/constants.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/__init__.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/action_schema.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/actions.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/assets.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/connections.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/context_grounding.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/context_grounding_index.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/exceptions.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/interrupt_models.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/job.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/llm_gateway.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/processes.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/queues.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/py.typed +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/src/uipath/tracing/_utils.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/tests/__init__.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/tests/cli/test_init.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/tests/conftest.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/tests/sdk/services/test_llm_integration.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/tests/sdk/services/test_llm_service.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/tests/sdk/services/test_uipath_llm_integration.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/tests/sdk/test_config.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/tests/tracing/test_span_utils.py +0 -0
- {uipath-2.0.12 → uipath-2.0.14}/tests/tracing/test_traced.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uipath
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.14
|
|
4
4
|
Summary: Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools.
|
|
5
5
|
Project-URL: Homepage, https://uipath.com
|
|
6
6
|
Project-URL: Repository, https://github.com/UiPath/uipath-python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "uipath"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.14"
|
|
4
4
|
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
|
|
5
5
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -115,9 +115,9 @@ def run(entrypoint: Optional[str], input: Optional[str], resume: bool) -> None:
|
|
|
115
115
|
|
|
116
116
|
# Handle result from middleware
|
|
117
117
|
if result.error_message:
|
|
118
|
-
click.echo(result.error_message)
|
|
118
|
+
click.echo(result.error_message, err=True)
|
|
119
119
|
if result.should_include_stacktrace:
|
|
120
|
-
click.echo(traceback.format_exc())
|
|
120
|
+
click.echo(traceback.format_exc(), err=True)
|
|
121
121
|
click.get_current_context().exit(1)
|
|
122
122
|
|
|
123
123
|
if result.info_message:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
-
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, Sequence
|
|
5
6
|
|
|
6
7
|
from httpx import Client
|
|
7
8
|
from opentelemetry.sdk.trace import ReadableSpan
|
|
@@ -30,7 +31,7 @@ class LlmOpsHttpExporter(SpanExporter):
|
|
|
30
31
|
|
|
31
32
|
self.http_client = Client(headers=self.headers)
|
|
32
33
|
|
|
33
|
-
def export(self, spans: Sequence[ReadableSpan]):
|
|
34
|
+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
34
35
|
"""Export spans to UiPath LLM Ops."""
|
|
35
36
|
logger.debug(
|
|
36
37
|
f"Exporting {len(spans)} spans to {self.base_url}/llmopstenant_/api/Traces/spans"
|
|
@@ -39,23 +40,42 @@ class LlmOpsHttpExporter(SpanExporter):
|
|
|
39
40
|
span_list = [
|
|
40
41
|
_SpanUtils.otel_span_to_uipath_span(span).to_dict() for span in spans
|
|
41
42
|
]
|
|
43
|
+
url = self._build_url(span_list)
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
url = f"{self.base_url}/llmopstenant_/api/Traces/spans?traceId={trace_id}&source=Robots"
|
|
45
|
-
|
|
46
|
-
logger.debug("payload: ", json.dumps(span_list))
|
|
47
|
-
|
|
48
|
-
res = self.http_client.post(url, json=span_list)
|
|
45
|
+
logger.debug("Payload: %s", json.dumps(span_list))
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
return SpanExportResult.SUCCESS
|
|
52
|
-
else:
|
|
53
|
-
return SpanExportResult.FAILURE
|
|
47
|
+
return self._send_with_retries(url, span_list)
|
|
54
48
|
|
|
55
49
|
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
56
50
|
"""Force flush the exporter."""
|
|
57
51
|
return True
|
|
58
52
|
|
|
53
|
+
def _build_url(self, span_list: list[Dict[str, Any]]) -> str:
|
|
54
|
+
"""Construct the URL for the API request."""
|
|
55
|
+
trace_id = str(span_list[0]["TraceId"])
|
|
56
|
+
return f"{self.base_url}/llmopstenant_/api/Traces/spans?traceId={trace_id}&source=Robots"
|
|
57
|
+
|
|
58
|
+
def _send_with_retries(
|
|
59
|
+
self, url: str, payload: list[Dict[str, Any]], max_retries: int = 4
|
|
60
|
+
) -> SpanExportResult:
|
|
61
|
+
"""Send the HTTP request with retry logic."""
|
|
62
|
+
for attempt in range(max_retries):
|
|
63
|
+
try:
|
|
64
|
+
response = self.http_client.post(url, json=payload)
|
|
65
|
+
if response.status_code == 200:
|
|
66
|
+
return SpanExportResult.SUCCESS
|
|
67
|
+
else:
|
|
68
|
+
logger.warning(
|
|
69
|
+
f"Attempt {attempt + 1} failed with status code {response.status_code}: {response.text}"
|
|
70
|
+
)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"Attempt {attempt + 1} failed with exception: {e}")
|
|
73
|
+
|
|
74
|
+
if attempt < max_retries - 1:
|
|
75
|
+
time.sleep(1.5**attempt) # Exponential backoff
|
|
76
|
+
|
|
77
|
+
return SpanExportResult.FAILURE
|
|
78
|
+
|
|
59
79
|
def _get_base_url(self) -> str:
|
|
60
80
|
uipath_url = (
|
|
61
81
|
os.environ.get("UIPATH_URL")
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import importlib
|
|
1
2
|
import inspect
|
|
2
3
|
import json
|
|
3
4
|
import logging
|
|
4
5
|
from functools import wraps
|
|
5
|
-
from typing import Any, Callable, Optional
|
|
6
|
+
from typing import Any, Callable, List, Optional, Tuple
|
|
6
7
|
|
|
7
8
|
from opentelemetry import trace
|
|
8
9
|
from opentelemetry.sdk.trace import TracerProvider
|
|
@@ -18,9 +19,94 @@ trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(LlmOpsHttpExpo
|
|
|
18
19
|
tracer = trace.get_tracer(__name__)
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
"""
|
|
23
|
-
|
|
22
|
+
class TracingManager:
|
|
23
|
+
"""Static utility class to manage tracing implementations and decorated functions."""
|
|
24
|
+
|
|
25
|
+
# Registry to track original functions, decorated functions, and their parameters
|
|
26
|
+
# Each entry is (original_func, decorated_func, params)
|
|
27
|
+
_traced_registry: List[Tuple[Callable[..., Any], Callable[..., Any], Any]] = []
|
|
28
|
+
|
|
29
|
+
# Custom tracer implementation
|
|
30
|
+
_custom_tracer_implementation = None
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def get_custom_tracer_implementation(cls):
|
|
34
|
+
"""Get the currently set custom tracer implementation."""
|
|
35
|
+
return cls._custom_tracer_implementation
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def register_traced_function(cls, original_func, decorated_func, params):
|
|
39
|
+
"""Register a function decorated with @traced and its parameters.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
original_func: The original function before decoration
|
|
43
|
+
decorated_func: The function after decoration
|
|
44
|
+
params: The parameters used for tracing
|
|
45
|
+
"""
|
|
46
|
+
cls._traced_registry.append((original_func, decorated_func, params))
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def reapply_traced_decorator(cls, tracer_implementation):
|
|
50
|
+
"""Reapply a different tracer implementation to all functions previously decorated with @traced.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
tracer_implementation: A function that takes the same parameters as _opentelemetry_traced
|
|
54
|
+
and returns a decorator
|
|
55
|
+
"""
|
|
56
|
+
cls._custom_tracer_implementation = tracer_implementation
|
|
57
|
+
|
|
58
|
+
# Work with a copy of the registry to avoid modifying it during iteration
|
|
59
|
+
registry_copy = cls._traced_registry.copy()
|
|
60
|
+
|
|
61
|
+
for original_func, decorated_func, params in registry_copy:
|
|
62
|
+
# Apply the new decorator with the same parameters
|
|
63
|
+
new_decorated_func = tracer_implementation(**params)(original_func)
|
|
64
|
+
|
|
65
|
+
logger.debug(
|
|
66
|
+
f"Reapplying decorator to {original_func.__name__}, from {decorated_func.__name__}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# If this is a method on a class, we need to update the class
|
|
70
|
+
if hasattr(original_func, "__self__") and hasattr(
|
|
71
|
+
original_func, "__func__"
|
|
72
|
+
):
|
|
73
|
+
setattr(
|
|
74
|
+
original_func.__self__.__class__,
|
|
75
|
+
original_func.__name__,
|
|
76
|
+
new_decorated_func.__get__(
|
|
77
|
+
original_func.__self__, original_func.__self__.__class__
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
# Replace the function in its module
|
|
82
|
+
if hasattr(original_func, "__module__") and hasattr(
|
|
83
|
+
original_func, "__qualname__"
|
|
84
|
+
):
|
|
85
|
+
try:
|
|
86
|
+
module = importlib.import_module(original_func.__module__)
|
|
87
|
+
parts = original_func.__qualname__.split(".")
|
|
88
|
+
|
|
89
|
+
# Handle nested objects
|
|
90
|
+
obj = module
|
|
91
|
+
for part in parts[:-1]:
|
|
92
|
+
obj = getattr(obj, part)
|
|
93
|
+
|
|
94
|
+
setattr(obj, parts[-1], new_decorated_func)
|
|
95
|
+
|
|
96
|
+
# Update the registry entry for this function
|
|
97
|
+
# Find the index and replace with updated entry
|
|
98
|
+
for i, (orig, _dec, _p) in enumerate(cls._traced_registry):
|
|
99
|
+
if orig is original_func:
|
|
100
|
+
cls._traced_registry[i] = (
|
|
101
|
+
original_func,
|
|
102
|
+
new_decorated_func,
|
|
103
|
+
params,
|
|
104
|
+
)
|
|
105
|
+
break
|
|
106
|
+
except (ImportError, AttributeError) as e:
|
|
107
|
+
# Log the error but continue processing other functions
|
|
108
|
+
logger.warning(f"Error reapplying decorator: {e}")
|
|
109
|
+
continue
|
|
24
110
|
|
|
25
111
|
|
|
26
112
|
def _default_input_processor(inputs):
|
|
@@ -33,23 +119,9 @@ def _default_output_processor(outputs):
|
|
|
33
119
|
return {"redacted": "Output data not logged for privacy/security"}
|
|
34
120
|
|
|
35
121
|
|
|
36
|
-
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
_decorators: dict[str, Any] = {}
|
|
40
|
-
_active_decorator = "opentelemetry"
|
|
41
|
-
|
|
42
|
-
@classmethod
|
|
43
|
-
def register_decorator(cls, name, decorator_factory):
|
|
44
|
-
"""Register a decorator factory function with a name."""
|
|
45
|
-
cls._decorators[name] = decorator_factory
|
|
46
|
-
cls._active_decorator = name
|
|
47
|
-
return cls
|
|
48
|
-
|
|
49
|
-
@classmethod
|
|
50
|
-
def get_decorator(cls):
|
|
51
|
-
"""Get the currently active decorator factory."""
|
|
52
|
-
return cls._decorators.get(cls._active_decorator)
|
|
122
|
+
def wait_for_tracers():
|
|
123
|
+
"""Wait for all tracers to finish."""
|
|
124
|
+
trace.get_tracer_provider().shutdown() # type: ignore
|
|
53
125
|
|
|
54
126
|
|
|
55
127
|
def _opentelemetry_traced(
|
|
@@ -58,6 +130,8 @@ def _opentelemetry_traced(
|
|
|
58
130
|
input_processor: Optional[Callable[..., Any]] = None,
|
|
59
131
|
output_processor: Optional[Callable[..., Any]] = None,
|
|
60
132
|
):
|
|
133
|
+
"""Default tracer implementation using OpenTelemetry."""
|
|
134
|
+
|
|
61
135
|
def decorator(func):
|
|
62
136
|
@wraps(func)
|
|
63
137
|
def sync_wrapper(*args, **kwargs):
|
|
@@ -78,9 +152,7 @@ def _opentelemetry_traced(
|
|
|
78
152
|
if input_processor is not None:
|
|
79
153
|
processed_inputs = input_processor(json.loads(inputs))
|
|
80
154
|
inputs = json.dumps(processed_inputs, default=str)
|
|
81
|
-
|
|
82
155
|
span.set_attribute("inputs", inputs)
|
|
83
|
-
|
|
84
156
|
try:
|
|
85
157
|
result = func(*args, **kwargs)
|
|
86
158
|
# Process output if processor is provided
|
|
@@ -115,9 +187,7 @@ def _opentelemetry_traced(
|
|
|
115
187
|
if input_processor is not None:
|
|
116
188
|
processed_inputs = input_processor(json.loads(inputs))
|
|
117
189
|
inputs = json.dumps(processed_inputs, default=str)
|
|
118
|
-
|
|
119
190
|
span.set_attribute("inputs", inputs)
|
|
120
|
-
|
|
121
191
|
try:
|
|
122
192
|
result = await func(*args, **kwargs)
|
|
123
193
|
# Process output if processor is provided
|
|
@@ -152,9 +222,7 @@ def _opentelemetry_traced(
|
|
|
152
222
|
if input_processor is not None:
|
|
153
223
|
processed_inputs = input_processor(json.loads(inputs))
|
|
154
224
|
inputs = json.dumps(processed_inputs, default=str)
|
|
155
|
-
|
|
156
225
|
span.set_attribute("inputs", inputs)
|
|
157
|
-
|
|
158
226
|
outputs = []
|
|
159
227
|
try:
|
|
160
228
|
for item in func(*args, **kwargs):
|
|
@@ -195,9 +263,7 @@ def _opentelemetry_traced(
|
|
|
195
263
|
if input_processor is not None:
|
|
196
264
|
processed_inputs = input_processor(json.loads(inputs))
|
|
197
265
|
inputs = json.dumps(processed_inputs, default=str)
|
|
198
|
-
|
|
199
266
|
span.set_attribute("inputs", inputs)
|
|
200
|
-
|
|
201
267
|
outputs = []
|
|
202
268
|
try:
|
|
203
269
|
async for item in func(*args, **kwargs):
|
|
@@ -254,16 +320,28 @@ def traced(
|
|
|
254
320
|
# Apply default processors selectively based on hide flags
|
|
255
321
|
if hide_input:
|
|
256
322
|
input_processor = _default_input_processor
|
|
257
|
-
|
|
258
323
|
if hide_output:
|
|
259
324
|
output_processor = _default_output_processor
|
|
260
325
|
|
|
261
|
-
|
|
326
|
+
# Store the parameters for later reapplication
|
|
327
|
+
params = {
|
|
328
|
+
"run_type": run_type,
|
|
329
|
+
"span_type": span_type,
|
|
330
|
+
"input_processor": input_processor,
|
|
331
|
+
"output_processor": output_processor,
|
|
332
|
+
}
|
|
262
333
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
334
|
+
# Check for custom implementation first
|
|
335
|
+
custom_implementation = TracingManager.get_custom_tracer_implementation()
|
|
336
|
+
tracer_impl: Any = (
|
|
337
|
+
custom_implementation if custom_implementation else _opentelemetry_traced
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def decorator(func):
|
|
341
|
+
# Decorate the function
|
|
342
|
+
decorated_func = tracer_impl(**params)(func)
|
|
343
|
+
# Register both original and decorated function with parameters
|
|
344
|
+
TracingManager.register_traced_function(func, decorated_func, params)
|
|
345
|
+
return decorated_func
|
|
346
|
+
|
|
347
|
+
return decorator
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
6
|
+
from opentelemetry.sdk.trace.export import SpanExportResult
|
|
7
|
+
|
|
8
|
+
from uipath.tracing._otel_exporters import LlmOpsHttpExporter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def mock_env_vars():
|
|
13
|
+
"""Fixture to set and clean up environment variables for testing."""
|
|
14
|
+
original_values = {}
|
|
15
|
+
|
|
16
|
+
# Save original values
|
|
17
|
+
for var in ["UIPATH_URL", "UIPATH_ACCESS_TOKEN"]:
|
|
18
|
+
original_values[var] = os.environ.get(var)
|
|
19
|
+
|
|
20
|
+
# Set test values
|
|
21
|
+
os.environ["UIPATH_URL"] = "https://test.uipath.com/org/tenant/"
|
|
22
|
+
os.environ["UIPATH_ACCESS_TOKEN"] = "test-token"
|
|
23
|
+
|
|
24
|
+
yield
|
|
25
|
+
|
|
26
|
+
# Restore original values
|
|
27
|
+
for var, value in original_values.items():
|
|
28
|
+
if value is None:
|
|
29
|
+
if var in os.environ:
|
|
30
|
+
del os.environ[var]
|
|
31
|
+
else:
|
|
32
|
+
os.environ[var] = value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def mock_span():
|
|
37
|
+
"""Create a mock ReadableSpan for testing."""
|
|
38
|
+
span = MagicMock(spec=ReadableSpan)
|
|
39
|
+
return span
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture
|
|
43
|
+
def exporter(mock_env_vars):
|
|
44
|
+
"""Create an exporter instance for testing."""
|
|
45
|
+
with patch("uipath.tracing._otel_exporters.Client"):
|
|
46
|
+
exporter = LlmOpsHttpExporter()
|
|
47
|
+
# Mock _build_url to include query parameters as in the actual implementation
|
|
48
|
+
exporter._build_url = MagicMock( # type: ignore
|
|
49
|
+
return_value="https://test.uipath.com/org/tenant/llmopstenant_/api/Traces/spans?traceId=test-trace-id&source=Robots"
|
|
50
|
+
)
|
|
51
|
+
yield exporter
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_init_with_env_vars(mock_env_vars):
|
|
55
|
+
"""Test initialization with environment variables."""
|
|
56
|
+
with patch("uipath.tracing._otel_exporters.Client"):
|
|
57
|
+
exporter = LlmOpsHttpExporter()
|
|
58
|
+
|
|
59
|
+
assert exporter.base_url == "https://test.uipath.com/org/tenant"
|
|
60
|
+
assert exporter.auth_token == "test-token"
|
|
61
|
+
assert exporter.headers == {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
"Authorization": "Bearer test-token",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_init_with_default_url():
|
|
68
|
+
"""Test initialization with default URL when environment variable is not set."""
|
|
69
|
+
with (
|
|
70
|
+
patch("uipath.tracing._otel_exporters.Client"),
|
|
71
|
+
patch.dict(os.environ, {"UIPATH_ACCESS_TOKEN": "test-token"}, clear=True),
|
|
72
|
+
):
|
|
73
|
+
exporter = LlmOpsHttpExporter()
|
|
74
|
+
|
|
75
|
+
assert exporter.base_url == "https://cloud.uipath.com/dummyOrg/dummyTennant"
|
|
76
|
+
assert exporter.auth_token == "test-token"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_export_success(exporter, mock_span):
|
|
80
|
+
"""Test successful export of spans."""
|
|
81
|
+
mock_uipath_span = MagicMock()
|
|
82
|
+
mock_uipath_span.to_dict.return_value = {"span": "data", "TraceId": "test-trace-id"}
|
|
83
|
+
|
|
84
|
+
with patch(
|
|
85
|
+
"uipath.tracing._otel_exporters._SpanUtils.otel_span_to_uipath_span",
|
|
86
|
+
return_value=mock_uipath_span,
|
|
87
|
+
):
|
|
88
|
+
mock_response = MagicMock()
|
|
89
|
+
mock_response.status_code = 200
|
|
90
|
+
exporter.http_client.post.return_value = mock_response
|
|
91
|
+
|
|
92
|
+
result = exporter.export([mock_span])
|
|
93
|
+
|
|
94
|
+
assert result == SpanExportResult.SUCCESS
|
|
95
|
+
exporter._build_url.assert_called_once_with(
|
|
96
|
+
[{"span": "data", "TraceId": "test-trace-id"}]
|
|
97
|
+
)
|
|
98
|
+
exporter.http_client.post.assert_called_once_with(
|
|
99
|
+
"https://test.uipath.com/org/tenant/llmopstenant_/api/Traces/spans?traceId=test-trace-id&source=Robots",
|
|
100
|
+
json=[{"span": "data", "TraceId": "test-trace-id"}],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_export_failure(exporter, mock_span):
|
|
105
|
+
"""Test export failure with multiple retries."""
|
|
106
|
+
mock_uipath_span = MagicMock()
|
|
107
|
+
mock_uipath_span.to_dict.return_value = {"span": "data"}
|
|
108
|
+
|
|
109
|
+
with patch(
|
|
110
|
+
"uipath.tracing._otel_exporters._SpanUtils.otel_span_to_uipath_span",
|
|
111
|
+
return_value=mock_uipath_span,
|
|
112
|
+
):
|
|
113
|
+
mock_response = MagicMock()
|
|
114
|
+
mock_response.status_code = 500
|
|
115
|
+
mock_response.text = "Internal Server Error"
|
|
116
|
+
exporter.http_client.post.return_value = mock_response
|
|
117
|
+
|
|
118
|
+
with patch("uipath.tracing._otel_exporters.time.sleep") as mock_sleep:
|
|
119
|
+
result = exporter.export([mock_span])
|
|
120
|
+
|
|
121
|
+
assert result == SpanExportResult.FAILURE
|
|
122
|
+
assert exporter.http_client.post.call_count == 4 # Default max_retries is 3
|
|
123
|
+
assert (
|
|
124
|
+
mock_sleep.call_count == 3
|
|
125
|
+
) # Should sleep between retries (except after the last one)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_export_exception(exporter, mock_span):
|
|
129
|
+
"""Test export with exceptions during HTTP request."""
|
|
130
|
+
mock_uipath_span = MagicMock()
|
|
131
|
+
mock_uipath_span.to_dict.return_value = {"span": "data"}
|
|
132
|
+
|
|
133
|
+
with patch(
|
|
134
|
+
"uipath.tracing._otel_exporters._SpanUtils.otel_span_to_uipath_span",
|
|
135
|
+
return_value=mock_uipath_span,
|
|
136
|
+
):
|
|
137
|
+
exporter.http_client.post.side_effect = Exception("Connection error")
|
|
138
|
+
|
|
139
|
+
with patch("uipath.tracing._otel_exporters.time.sleep"):
|
|
140
|
+
result = exporter.export([mock_span])
|
|
141
|
+
|
|
142
|
+
assert result == SpanExportResult.FAILURE
|
|
143
|
+
assert exporter.http_client.post.call_count == 4 # Default max_retries is 3
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_force_flush(exporter):
|
|
147
|
+
"""Test force_flush returns True."""
|
|
148
|
+
assert exporter.force_flush() is True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_get_base_url():
|
|
152
|
+
"""Test _get_base_url method with different environment configurations."""
|
|
153
|
+
# Test with environment variable set
|
|
154
|
+
with patch.dict(
|
|
155
|
+
os.environ, {"UIPATH_URL": "https://custom.uipath.com/org/tenant/"}, clear=True
|
|
156
|
+
):
|
|
157
|
+
with patch("uipath.tracing._otel_exporters.Client"):
|
|
158
|
+
exporter = LlmOpsHttpExporter()
|
|
159
|
+
assert exporter.base_url == "https://custom.uipath.com/org/tenant"
|
|
160
|
+
|
|
161
|
+
# Test with environment variable set but with no trailing slash
|
|
162
|
+
with patch.dict(
|
|
163
|
+
os.environ, {"UIPATH_URL": "https://custom.uipath.com/org/tenant"}, clear=True
|
|
164
|
+
):
|
|
165
|
+
with patch("uipath.tracing._otel_exporters.Client"):
|
|
166
|
+
exporter = LlmOpsHttpExporter()
|
|
167
|
+
assert exporter.base_url == "https://custom.uipath.com/org/tenant"
|
|
168
|
+
|
|
169
|
+
# Test with no environment variable
|
|
170
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
171
|
+
with patch("uipath.tracing._otel_exporters.Client"):
|
|
172
|
+
exporter = LlmOpsHttpExporter()
|
|
173
|
+
assert exporter.base_url == "https://cloud.uipath.com/dummyOrg/dummyTennant"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_send_with_retries_success():
|
|
177
|
+
"""Test _send_with_retries method with successful response."""
|
|
178
|
+
with patch("uipath.tracing._otel_exporters.Client"):
|
|
179
|
+
exporter = LlmOpsHttpExporter()
|
|
180
|
+
|
|
181
|
+
mock_response = MagicMock()
|
|
182
|
+
mock_response.status_code = 200
|
|
183
|
+
exporter.http_client.post.return_value = mock_response # type: ignore
|
|
184
|
+
|
|
185
|
+
result = exporter._send_with_retries("http://example.com", [{"span": "data"}])
|
|
186
|
+
|
|
187
|
+
assert result == SpanExportResult.SUCCESS
|
|
188
|
+
exporter.http_client.post.assert_called_once_with( # type: ignore
|
|
189
|
+
"http://example.com", json=[{"span": "data"}]
|
|
190
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
|
|
3
|
+
from uipath.tracing._traced import TracingManager, traced
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Custom wrapper that does nothing
|
|
7
|
+
def donothing_custom_tracer(**kwargs):
|
|
8
|
+
def decorator(func):
|
|
9
|
+
@wraps(func)
|
|
10
|
+
def wrapper(*args, **kwargs):
|
|
11
|
+
# Simple implementation that just adds a marker to the result
|
|
12
|
+
result = func(*args, **kwargs)
|
|
13
|
+
return result
|
|
14
|
+
|
|
15
|
+
return wrapper
|
|
16
|
+
|
|
17
|
+
return decorator
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Helper function for testing custom tracer
|
|
21
|
+
def simple_custom_tracer(**kwargs):
|
|
22
|
+
def decorator(func):
|
|
23
|
+
@wraps(func)
|
|
24
|
+
def wrapper(*args, **kwargs):
|
|
25
|
+
# Simple implementation that just adds a marker to the result
|
|
26
|
+
result = func(*args, **kwargs)
|
|
27
|
+
if isinstance(result, dict):
|
|
28
|
+
result["custom_tracer_used"] = True
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
return wrapper
|
|
32
|
+
|
|
33
|
+
return decorator
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Helper function for testing custom tracer with method
|
|
37
|
+
def custom_method_tracer(**kwargs):
|
|
38
|
+
def decorator(func):
|
|
39
|
+
@wraps(func)
|
|
40
|
+
def wrapper(*args, **kwargs):
|
|
41
|
+
result = func(*args, **kwargs)
|
|
42
|
+
if isinstance(result, dict):
|
|
43
|
+
result["custom_method_tracer_used"] = True
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
return wrapper
|
|
47
|
+
|
|
48
|
+
return decorator
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Helper function for testing with counter
|
|
52
|
+
def custom_tracer_with_counter(call_counter, **kwargs):
|
|
53
|
+
def decorator(func):
|
|
54
|
+
@wraps(func)
|
|
55
|
+
def wrapper(*args, **kwargs):
|
|
56
|
+
call_counter["count"] += 1
|
|
57
|
+
result = func(*args, **kwargs)
|
|
58
|
+
if isinstance(result, dict):
|
|
59
|
+
result["custom_tracer_id"] = call_counter["count"]
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
return wrapper
|
|
63
|
+
|
|
64
|
+
return decorator
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Define the test classes
|
|
68
|
+
class TestClassForMethodTest:
|
|
69
|
+
@traced()
|
|
70
|
+
def sample_method(self, x, y):
|
|
71
|
+
return {"product": x * y}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Module level functions for function test
|
|
75
|
+
@traced()
|
|
76
|
+
def func1_for_test(x):
|
|
77
|
+
return {"result": x * 2}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@traced()
|
|
81
|
+
def func2_for_test(x):
|
|
82
|
+
return {"result": x * 3}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Define a function with @traced
|
|
86
|
+
@traced()
|
|
87
|
+
def sample_function():
|
|
88
|
+
return {"status": "success"}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_tracing_manager_custom_implementation():
|
|
92
|
+
"""Test setting and getting a custom tracer implementation."""
|
|
93
|
+
# Set the custom implementation
|
|
94
|
+
TracingManager.reapply_traced_decorator(simple_custom_tracer)
|
|
95
|
+
|
|
96
|
+
# Get the implementation and verify it's the same one
|
|
97
|
+
impl = TracingManager.get_custom_tracer_implementation()
|
|
98
|
+
assert impl is simple_custom_tracer
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_traced_with_custom_implementation():
|
|
102
|
+
"""Test that @traced uses a custom implementation when provided."""
|
|
103
|
+
# Set the custom implementation
|
|
104
|
+
TracingManager.reapply_traced_decorator(simple_custom_tracer)
|
|
105
|
+
|
|
106
|
+
# Call the function and verify the custom implementation was used
|
|
107
|
+
result = sample_function()
|
|
108
|
+
assert "custom_tracer_used" in result
|
|
109
|
+
assert result["custom_tracer_used"] is True
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_reapply_traced_decorator_to_class_method():
|
|
113
|
+
"""Test reapply_traced_decorator with class methods."""
|
|
114
|
+
TracingManager.reapply_traced_decorator(donothing_custom_tracer)
|
|
115
|
+
|
|
116
|
+
# Create instance and call with default implementation
|
|
117
|
+
instance = TestClassForMethodTest()
|
|
118
|
+
result1 = instance.sample_method(2, 3)
|
|
119
|
+
assert result1 == {"product": 6}
|
|
120
|
+
|
|
121
|
+
# Apply our custom implementation
|
|
122
|
+
TracingManager.reapply_traced_decorator(custom_method_tracer)
|
|
123
|
+
|
|
124
|
+
# Create a NEW instance which will use the updated class method
|
|
125
|
+
new_instance = TestClassForMethodTest()
|
|
126
|
+
result2 = new_instance.sample_method(2, 3)
|
|
127
|
+
|
|
128
|
+
# Verify the result
|
|
129
|
+
assert "product" in result2
|
|
130
|
+
assert result2["product"] == 6
|
|
131
|
+
assert "custom_method_tracer_used" in result2
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_reapply_to_module_level_functions():
|
|
135
|
+
"""Test reapply_traced_decorator with module level functions."""
|
|
136
|
+
|
|
137
|
+
TracingManager.reapply_traced_decorator(donothing_custom_tracer)
|
|
138
|
+
|
|
139
|
+
# First call with default implementation
|
|
140
|
+
assert func1_for_test(5) == {"result": 10}
|
|
141
|
+
assert func2_for_test(5) == {"result": 15}
|
|
142
|
+
|
|
143
|
+
# Counter to track custom tracer calls
|
|
144
|
+
call_counter = {"count": 0}
|
|
145
|
+
|
|
146
|
+
# Reapply with the custom implementation
|
|
147
|
+
TracingManager.reapply_traced_decorator(
|
|
148
|
+
lambda **kwargs: custom_tracer_with_counter(call_counter, **kwargs)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Call the functions directly - they should now use the updated implementation
|
|
152
|
+
result1 = func1_for_test(5)
|
|
153
|
+
result2 = func2_for_test(5)
|
|
154
|
+
|
|
155
|
+
# Verify the custom implementation was applied
|
|
156
|
+
assert "result" in result1
|
|
157
|
+
assert result1["result"] == 10
|
|
158
|
+
assert "custom_tracer_id" in result1
|
|
159
|
+
|
|
160
|
+
assert "result" in result2
|
|
161
|
+
assert result2["result"] == 15
|
|
162
|
+
assert "custom_tracer_id" in result2
|
|
163
|
+
|
|
164
|
+
# Verify both functions were processed
|
|
165
|
+
assert call_counter["count"] == 2
|