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.

Files changed (126) hide show
  1. {uipath-2.0.12 → uipath-2.0.14}/PKG-INFO +1 -1
  2. {uipath-2.0.12 → uipath-2.0.14}/pyproject.toml +1 -1
  3. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_run.py +2 -2
  4. uipath-2.0.14/src/uipath/tracing/__init__.py +3 -0
  5. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/tracing/_otel_exporters.py +32 -12
  6. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/tracing/_traced.py +116 -38
  7. uipath-2.0.14/tests/tracing/test_otel_exporters.py +190 -0
  8. uipath-2.0.14/tests/tracing/test_tracing_manager.py +165 -0
  9. {uipath-2.0.12 → uipath-2.0.14}/uv.lock +1509 -1509
  10. uipath-2.0.12/src/uipath/tracing/__init__.py +0 -3
  11. {uipath-2.0.12 → uipath-2.0.14}/.cursorrules +0 -0
  12. {uipath-2.0.12 → uipath-2.0.14}/.editorconfig +0 -0
  13. {uipath-2.0.12 → uipath-2.0.14}/.gitattributes +0 -0
  14. {uipath-2.0.12 → uipath-2.0.14}/.github/workflows/build.yml +0 -0
  15. {uipath-2.0.12 → uipath-2.0.14}/.github/workflows/cd.yml +0 -0
  16. {uipath-2.0.12 → uipath-2.0.14}/.github/workflows/ci.yml +0 -0
  17. {uipath-2.0.12 → uipath-2.0.14}/.github/workflows/commitlint.yml +0 -0
  18. {uipath-2.0.12 → uipath-2.0.14}/.github/workflows/lint.yml +0 -0
  19. {uipath-2.0.12 → uipath-2.0.14}/.github/workflows/test.yml +0 -0
  20. {uipath-2.0.12 → uipath-2.0.14}/.gitignore +0 -0
  21. {uipath-2.0.12 → uipath-2.0.14}/.pre-commit-config.yaml +0 -0
  22. {uipath-2.0.12 → uipath-2.0.14}/.python-version +0 -0
  23. {uipath-2.0.12 → uipath-2.0.14}/.vscode/extensions.json +0 -0
  24. {uipath-2.0.12 → uipath-2.0.14}/.vscode/settings.json +0 -0
  25. {uipath-2.0.12 → uipath-2.0.14}/CONTRIBUTING.md +0 -0
  26. {uipath-2.0.12 → uipath-2.0.14}/LICENSE +0 -0
  27. {uipath-2.0.12 → uipath-2.0.14}/README.md +0 -0
  28. {uipath-2.0.12 → uipath-2.0.14}/docs/CONTRIBUTING.md +0 -0
  29. {uipath-2.0.12 → uipath-2.0.14}/docs/actions.md +0 -0
  30. {uipath-2.0.12 → uipath-2.0.14}/docs/assets/uipath-logo.svg +0 -0
  31. {uipath-2.0.12 → uipath-2.0.14}/docs/assets.md +0 -0
  32. {uipath-2.0.12 → uipath-2.0.14}/docs/buckets.md +0 -0
  33. {uipath-2.0.12 → uipath-2.0.14}/docs/connections.md +0 -0
  34. {uipath-2.0.12 → uipath-2.0.14}/docs/context_grounding.md +0 -0
  35. {uipath-2.0.12 → uipath-2.0.14}/docs/getting_started_agent.md +0 -0
  36. {uipath-2.0.12 → uipath-2.0.14}/docs/getting_started_cli.md +0 -0
  37. {uipath-2.0.12 → uipath-2.0.14}/docs/getting_started_cloud.md +0 -0
  38. {uipath-2.0.12 → uipath-2.0.14}/docs/getting_started_sdk.md +0 -0
  39. {uipath-2.0.12 → uipath-2.0.14}/docs/index.md +0 -0
  40. {uipath-2.0.12 → uipath-2.0.14}/docs/jobs.md +0 -0
  41. {uipath-2.0.12 → uipath-2.0.14}/docs/processes.md +0 -0
  42. {uipath-2.0.12 → uipath-2.0.14}/docs/queues.md +0 -0
  43. {uipath-2.0.12 → uipath-2.0.14}/docs/sdk.md +0 -0
  44. {uipath-2.0.12 → uipath-2.0.14}/justfile +0 -0
  45. {uipath-2.0.12 → uipath-2.0.14}/mkdocs.yml +0 -0
  46. {uipath-2.0.12 → uipath-2.0.14}/py.typed +0 -0
  47. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/__init__.py +0 -0
  48. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/README.md +0 -0
  49. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/__init__.py +0 -0
  50. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/_auth_server.py +0 -0
  51. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/_models.py +0 -0
  52. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/_oidc_utils.py +0 -0
  53. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/_portal_service.py +0 -0
  54. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/_utils.py +0 -0
  55. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/auth_config.json +0 -0
  56. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/index.html +0 -0
  57. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/localhost.crt +0 -0
  58. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_auth/localhost.key +0 -0
  59. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_runtime/_contracts.py +0 -0
  60. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_runtime/_logging.py +0 -0
  61. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_runtime/_runtime.py +0 -0
  62. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_templates/.psmdcp.template +0 -0
  63. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_templates/.rels.template +0 -0
  64. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_templates/[Content_Types].xml.template +0 -0
  65. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_templates/main.py.template +0 -0
  66. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_templates/package.nuspec.template +0 -0
  67. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_utils/_common.py +0 -0
  68. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_utils/_input_args.py +0 -0
  69. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/_utils/_parse_ast.py +0 -0
  70. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_auth.py +0 -0
  71. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_deploy.py +0 -0
  72. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_init.py +0 -0
  73. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_new.py +0 -0
  74. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_pack.py +0 -0
  75. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/cli_publish.py +0 -0
  76. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_cli/middlewares.py +0 -0
  77. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_config.py +0 -0
  78. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_execution_context.py +0 -0
  79. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_folder_context.py +0 -0
  80. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/__init__.py +0 -0
  81. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/_base_service.py +0 -0
  82. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/actions_service.py +0 -0
  83. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/api_client.py +0 -0
  84. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/assets_service.py +0 -0
  85. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/buckets_service.py +0 -0
  86. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/connections_service.py +0 -0
  87. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/connections_service.pyi +0 -0
  88. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/context_grounding_service.py +0 -0
  89. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/folder_service.py +0 -0
  90. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/jobs_service.py +0 -0
  91. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/llm_gateway_service.py +0 -0
  92. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/processes_service.py +0 -0
  93. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_services/queues_service.py +0 -0
  94. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_uipath.py +0 -0
  95. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/__init__.py +0 -0
  96. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/_endpoint.py +0 -0
  97. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/_infer_bindings.py +0 -0
  98. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/_logs.py +0 -0
  99. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/_request_override.py +0 -0
  100. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/_request_spec.py +0 -0
  101. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/_user_agent.py +0 -0
  102. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/_utils/constants.py +0 -0
  103. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/__init__.py +0 -0
  104. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/action_schema.py +0 -0
  105. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/actions.py +0 -0
  106. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/assets.py +0 -0
  107. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/connections.py +0 -0
  108. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/context_grounding.py +0 -0
  109. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/context_grounding_index.py +0 -0
  110. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/exceptions.py +0 -0
  111. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/interrupt_models.py +0 -0
  112. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/job.py +0 -0
  113. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/llm_gateway.py +0 -0
  114. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/processes.py +0 -0
  115. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/models/queues.py +0 -0
  116. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/py.typed +0 -0
  117. {uipath-2.0.12 → uipath-2.0.14}/src/uipath/tracing/_utils.py +0 -0
  118. {uipath-2.0.12 → uipath-2.0.14}/tests/__init__.py +0 -0
  119. {uipath-2.0.12 → uipath-2.0.14}/tests/cli/test_init.py +0 -0
  120. {uipath-2.0.12 → uipath-2.0.14}/tests/conftest.py +0 -0
  121. {uipath-2.0.12 → uipath-2.0.14}/tests/sdk/services/test_llm_integration.py +0 -0
  122. {uipath-2.0.12 → uipath-2.0.14}/tests/sdk/services/test_llm_service.py +0 -0
  123. {uipath-2.0.12 → uipath-2.0.14}/tests/sdk/services/test_uipath_llm_integration.py +0 -0
  124. {uipath-2.0.12 → uipath-2.0.14}/tests/sdk/test_config.py +0 -0
  125. {uipath-2.0.12 → uipath-2.0.14}/tests/tracing/test_span_utils.py +0 -0
  126. {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.12
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.12"
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:
@@ -0,0 +1,3 @@
1
+ from ._traced import TracingManager, traced, wait_for_tracers # noqa: D104
2
+
3
+ __all__ = ["TracingManager", "traced", "wait_for_tracers"]
@@ -1,7 +1,8 @@
1
1
  import json
2
2
  import logging
3
3
  import os
4
- from typing import Sequence
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
- trace_id = str(span_list[0]["TraceId"])
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
- if res.status_code == 200:
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
- def wait_for_tracers():
22
- """Wait for all tracers to finish."""
23
- trace.get_tracer_provider().shutdown() # type: ignore
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
- class TracedDecoratorRegistry:
37
- """Registry for tracing decorators."""
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
- decorator_factory = TracedDecoratorRegistry.get_decorator()
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
- if decorator_factory:
264
- return decorator_factory(run_type, span_type, input_processor, output_processor)
265
- else:
266
- # Fallback to original implementation if no active decorator
267
- return _opentelemetry_traced(
268
- run_type, span_type, input_processor, output_processor
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