uipath-core 0.0.2__py3-none-any.whl

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.
@@ -0,0 +1,9 @@
1
+ """UiPath Core Package."""
2
+
3
+ from uipath.core.tracing.decorators import traced
4
+ from uipath.core.tracing.manager import UiPathTracingManager
5
+
6
+ __all__ = [
7
+ "traced",
8
+ "UiPathTracingManager",
9
+ ]
uipath/core/py.typed ADDED
File without changes
@@ -0,0 +1,15 @@
1
+ """OpenTelemetry tracing module with UiPath integration.
2
+
3
+ This module provides decorators and utilities for instrumenting Python functions
4
+ with OpenTelemetry tracing, including custom processors for UiPath execution tracking.
5
+ """
6
+
7
+ from uipath.core.tracing.context import UiPathTraceContext
8
+ from uipath.core.tracing.decorators import traced
9
+ from uipath.core.tracing.manager import UiPathTracingManager
10
+
11
+ __all__ = [
12
+ "traced",
13
+ "UiPathTraceContext",
14
+ "UiPathTracingManager",
15
+ ]
@@ -0,0 +1,122 @@
1
+ """Helper utilities for the tracing module."""
2
+
3
+ import inspect
4
+ import json
5
+ from collections.abc import Callable
6
+ from dataclasses import asdict, is_dataclass
7
+ from datetime import datetime, timezone
8
+ from enum import Enum
9
+ from typing import Any, Dict, Mapping
10
+ from zoneinfo import ZoneInfo
11
+
12
+ from pydantic import BaseModel
13
+
14
+
15
+ def get_supported_params(
16
+ tracer_impl: Callable[..., Any],
17
+ params: Mapping[str, Any],
18
+ ) -> Dict[str, Any]:
19
+ """Extract the parameters supported by the tracer implementation."""
20
+ try:
21
+ sig = inspect.signature(tracer_impl)
22
+ except (TypeError, ValueError):
23
+ # If we can't inspect, pass all parameters and let the function handle it
24
+ return dict(params)
25
+
26
+ supported: Dict[str, Any] = {}
27
+ for name, value in params.items():
28
+ if value is not None and name in sig.parameters:
29
+ supported[name] = value
30
+ return supported
31
+
32
+
33
+ def _simple_serialize_defaults(obj):
34
+ # Handle Pydantic BaseModel instances
35
+ if hasattr(obj, "model_dump") and not isinstance(obj, type):
36
+ return obj.model_dump(exclude_none=True, mode="json")
37
+
38
+ # Handle classes - convert to schema representation
39
+ if isinstance(obj, type) and issubclass(obj, BaseModel):
40
+ return {
41
+ "__class__": obj.__name__,
42
+ "__module__": obj.__module__,
43
+ "schema": obj.model_json_schema(),
44
+ }
45
+ if hasattr(obj, "dict") and not isinstance(obj, type):
46
+ return obj.dict()
47
+ if hasattr(obj, "to_dict") and not isinstance(obj, type):
48
+ return obj.to_dict()
49
+
50
+ # Handle dataclasses
51
+ if is_dataclass(obj) and not isinstance(obj, type):
52
+ return asdict(obj)
53
+
54
+ # Handle enums
55
+ if isinstance(obj, Enum):
56
+ return _simple_serialize_defaults(obj.value)
57
+
58
+ if isinstance(obj, (set, tuple)):
59
+ if hasattr(obj, "_asdict") and callable(obj._asdict):
60
+ return obj._asdict()
61
+ return list(obj)
62
+
63
+ if isinstance(obj, datetime):
64
+ return obj.isoformat()
65
+
66
+ if isinstance(obj, (timezone, ZoneInfo)):
67
+ return obj.tzname(None)
68
+
69
+ # Allow JSON-serializable primitives to pass through unchanged
70
+ if obj is None or isinstance(obj, (bool, int, float, str)):
71
+ return obj
72
+
73
+ return str(obj)
74
+
75
+
76
+ def format_args_for_trace_json(
77
+ signature: inspect.Signature, *args: Any, **kwargs: Any
78
+ ) -> str:
79
+ """Return a JSON string of inputs from the function signature."""
80
+ result = format_args_for_trace(signature, *args, **kwargs)
81
+ return json.dumps(result, default=_simple_serialize_defaults)
82
+
83
+
84
+ def format_object_for_trace_json(
85
+ input_object: Any,
86
+ ) -> str:
87
+ """Return a JSON string of inputs from the function signature."""
88
+ return json.dumps(input_object, default=_simple_serialize_defaults)
89
+
90
+
91
+ def format_args_for_trace(
92
+ signature: inspect.Signature, *args: Any, **kwargs: Any
93
+ ) -> Dict[str, Any]:
94
+ try:
95
+ """Return a dictionary of inputs from the function signature."""
96
+ # Create a parameter mapping by partially binding the arguments
97
+
98
+ parameter_binding = signature.bind_partial(*args, **kwargs)
99
+
100
+ # Fill in default values for any unspecified parameters
101
+ parameter_binding.apply_defaults()
102
+
103
+ # Extract the input parameters, skipping special Python parameters
104
+ result = {}
105
+ for name, value in parameter_binding.arguments.items():
106
+ # Skip class and instance references
107
+ if name in ("self", "cls"):
108
+ continue
109
+
110
+ # Handle **kwargs parameters specially
111
+ param_info = signature.parameters.get(name)
112
+ if param_info and param_info.kind == inspect.Parameter.VAR_KEYWORD:
113
+ # Flatten nested kwargs directly into the result
114
+ if isinstance(value, dict):
115
+ result.update(value)
116
+ else:
117
+ # Regular parameter
118
+ result[name] = value
119
+
120
+ return result
121
+ except Exception:
122
+ return {"args": args, "kwargs": kwargs}
@@ -0,0 +1,23 @@
1
+ """Trace context information for tracing and debugging."""
2
+
3
+ from typing import Optional, Union
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class UiPathTraceContext(BaseModel):
9
+ """Trace context information for tracing and debugging."""
10
+
11
+ trace_id: Optional[str] = None
12
+ parent_span_id: Optional[str] = None
13
+ root_span_id: Optional[str] = None
14
+ org_id: Optional[str] = None
15
+ tenant_id: Optional[str] = None
16
+ job_id: Optional[str] = None
17
+ folder_key: Optional[str] = None
18
+ process_key: Optional[str] = None
19
+ enabled: Union[bool, str] = False
20
+ reference_id: Optional[str] = None
21
+
22
+
23
+ __all__ = ["UiPathTraceContext"]
@@ -0,0 +1,282 @@
1
+ """Tracing decorators for function instrumentation."""
2
+
3
+ import inspect
4
+ import json
5
+ import logging
6
+ import random
7
+ from functools import wraps
8
+ from typing import Any, Callable, Optional
9
+
10
+ from opentelemetry import trace
11
+ from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags
12
+ from opentelemetry.trace.status import StatusCode
13
+
14
+ from uipath.core.tracing._utils import (
15
+ format_args_for_trace_json,
16
+ format_object_for_trace_json,
17
+ get_supported_params,
18
+ )
19
+ from uipath.core.tracing.manager import UiPathTracingManager
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def _opentelemetry_traced(
25
+ name: Optional[str] = None,
26
+ run_type: Optional[str] = None,
27
+ span_type: Optional[str] = None,
28
+ input_processor: Optional[Callable[..., Any]] = None,
29
+ output_processor: Optional[Callable[..., Any]] = None,
30
+ recording: bool = True,
31
+ ):
32
+ """Default tracer implementation using OpenTelemetry."""
33
+
34
+ def decorator(func):
35
+ trace_name = name or func.__name__
36
+
37
+ def get_span():
38
+ if not recording:
39
+ # Create a valid but non-sampled trace context
40
+ # Generate a valid trace ID (not INVALID)
41
+ trace_id = random.getrandbits(128)
42
+ span_id = random.getrandbits(64)
43
+
44
+ non_sampled_context = SpanContext(
45
+ trace_id=trace_id,
46
+ span_id=span_id,
47
+ is_remote=False,
48
+ trace_flags=TraceFlags(0x00), # NOT sampled
49
+ )
50
+ non_recording = NonRecordingSpan(non_sampled_context)
51
+
52
+ # Make it active so children see it
53
+ span_cm = trace.use_span(non_recording)
54
+ span_cm.__enter__()
55
+ return span_cm, non_recording
56
+
57
+ # Normal recording span
58
+ ctx = UiPathTracingManager.get_parent_context()
59
+ span_cm = trace.get_tracer(__name__).start_as_current_span(
60
+ trace_name, context=ctx
61
+ )
62
+ span = span_cm.__enter__()
63
+ return span_cm, span
64
+
65
+ # --------- Sync wrapper ---------
66
+ @wraps(func)
67
+ def sync_wrapper(*args, **kwargs):
68
+ span_cm, span = get_span()
69
+ try:
70
+ span.set_attribute("span_type", span_type or "function_call_sync")
71
+ if run_type is not None:
72
+ span.set_attribute("run_type", run_type)
73
+
74
+ inputs = format_args_for_trace_json(
75
+ inspect.signature(func), *args, **kwargs
76
+ )
77
+ if input_processor:
78
+ processed_inputs = input_processor(json.loads(inputs))
79
+ inputs = json.dumps(processed_inputs, default=str)
80
+
81
+ span.set_attribute("input.mime_type", "application/json")
82
+ span.set_attribute("input.value", inputs)
83
+
84
+ result = func(*args, **kwargs)
85
+ output = output_processor(result) if output_processor else result
86
+
87
+ span.set_attribute("output.value", format_object_for_trace_json(output))
88
+ span.set_attribute("output.mime_type", "application/json")
89
+ return result
90
+ except Exception as e:
91
+ span.record_exception(e)
92
+ span.set_status(StatusCode.ERROR, str(e))
93
+ raise
94
+ finally:
95
+ if span_cm:
96
+ span_cm.__exit__(None, None, None)
97
+
98
+ # --------- Async wrapper ---------
99
+ @wraps(func)
100
+ async def async_wrapper(*args, **kwargs):
101
+ span_cm, span = get_span()
102
+ try:
103
+ span.set_attribute("span_type", span_type or "function_call_async")
104
+ if run_type is not None:
105
+ span.set_attribute("run_type", run_type)
106
+
107
+ inputs = format_args_for_trace_json(
108
+ inspect.signature(func), *args, **kwargs
109
+ )
110
+ if input_processor:
111
+ processed_inputs = input_processor(json.loads(inputs))
112
+ inputs = json.dumps(processed_inputs, default=str)
113
+
114
+ span.set_attribute("input.mime_type", "application/json")
115
+ span.set_attribute("input.value", inputs)
116
+
117
+ result = await func(*args, **kwargs)
118
+ output = output_processor(result) if output_processor else result
119
+
120
+ span.set_attribute("output.value", format_object_for_trace_json(output))
121
+ span.set_attribute("output.mime_type", "application/json")
122
+ return result
123
+ except Exception as e:
124
+ span.record_exception(e)
125
+ span.set_status(StatusCode.ERROR, str(e))
126
+ raise
127
+ finally:
128
+ if span_cm:
129
+ span_cm.__exit__(None, None, None)
130
+
131
+ # --------- Generator wrapper ---------
132
+ @wraps(func)
133
+ def generator_wrapper(*args, **kwargs):
134
+ span_cm, span = get_span()
135
+ try:
136
+ span.set_attribute(
137
+ "span_type", span_type or "function_call_generator_sync"
138
+ )
139
+ if run_type is not None:
140
+ span.set_attribute("run_type", run_type)
141
+
142
+ inputs = format_args_for_trace_json(
143
+ inspect.signature(func), *args, **kwargs
144
+ )
145
+ if input_processor:
146
+ processed_inputs = input_processor(json.loads(inputs))
147
+ inputs = json.dumps(processed_inputs, default=str)
148
+
149
+ span.set_attribute("input.mime_type", "application/json")
150
+ span.set_attribute("input.value", inputs)
151
+
152
+ outputs = []
153
+ for item in func(*args, **kwargs):
154
+ outputs.append(item)
155
+ span.add_event(f"Yielded: {item}")
156
+ yield item
157
+
158
+ output = output_processor(outputs) if output_processor else outputs
159
+ span.set_attribute("output.value", format_object_for_trace_json(output))
160
+ span.set_attribute("output.mime_type", "application/json")
161
+ except Exception as e:
162
+ span.record_exception(e)
163
+ span.set_status(StatusCode.ERROR, str(e))
164
+ raise
165
+ finally:
166
+ if span_cm:
167
+ span_cm.__exit__(None, None, None)
168
+
169
+ # --------- Async generator wrapper ---------
170
+ @wraps(func)
171
+ async def async_generator_wrapper(*args, **kwargs):
172
+ span_cm, span = get_span()
173
+ try:
174
+ span.set_attribute(
175
+ "span_type", span_type or "function_call_generator_async"
176
+ )
177
+ if run_type is not None:
178
+ span.set_attribute("run_type", run_type)
179
+
180
+ inputs = format_args_for_trace_json(
181
+ inspect.signature(func), *args, **kwargs
182
+ )
183
+ if input_processor:
184
+ processed_inputs = input_processor(json.loads(inputs))
185
+ inputs = json.dumps(processed_inputs, default=str)
186
+
187
+ span.set_attribute("input.mime_type", "application/json")
188
+ span.set_attribute("input.value", inputs)
189
+
190
+ outputs = []
191
+ async for item in func(*args, **kwargs):
192
+ outputs.append(item)
193
+ span.add_event(f"Yielded: {item}")
194
+ yield item
195
+
196
+ output = output_processor(outputs) if output_processor else outputs
197
+ span.set_attribute("output.value", format_object_for_trace_json(output))
198
+ span.set_attribute("output.mime_type", "application/json")
199
+ except Exception as e:
200
+ span.record_exception(e)
201
+ span.set_status(StatusCode.ERROR, str(e))
202
+ raise
203
+ finally:
204
+ if span_cm:
205
+ span_cm.__exit__(None, None, None)
206
+
207
+ if inspect.iscoroutinefunction(func):
208
+ return async_wrapper
209
+ elif inspect.isgeneratorfunction(func):
210
+ return generator_wrapper
211
+ elif inspect.isasyncgenfunction(func):
212
+ return async_generator_wrapper
213
+ else:
214
+ return sync_wrapper
215
+
216
+ return decorator
217
+
218
+
219
+ def traced(
220
+ name: Optional[str] = None,
221
+ run_type: Optional[str] = None,
222
+ span_type: Optional[str] = None,
223
+ input_processor: Optional[Callable[..., Any]] = None,
224
+ output_processor: Optional[Callable[..., Any]] = None,
225
+ hide_input: bool = False,
226
+ hide_output: bool = False,
227
+ recording: bool = True,
228
+ ):
229
+ """Decorator that will trace function invocations.
230
+
231
+ Args:
232
+ name: Optional custom name for the span
233
+ run_type: Optional string to categorize the run type
234
+ span_type: Optional string to categorize the span type
235
+ input_processor: Optional function to process function inputs before recording
236
+ Should accept a dictionary of inputs and return a processed dictionary
237
+ output_processor: Optional function to process function outputs before recording
238
+ Should accept the function output and return a processed value
239
+ hide_input: If True, don't log any input data
240
+ hide_output: If True, don't log any output data
241
+ recording: If False, current span and all child spans are not captured
242
+ """
243
+
244
+ # Apply default processors selectively based on hide flags
245
+ def _default_input_processor(inputs):
246
+ """Default input processor that doesn't log any actual input data."""
247
+ return {"redacted": "Input data not logged for privacy/security"}
248
+
249
+ def _default_output_processor(outputs):
250
+ """Default output processor that doesn't log any actual output data."""
251
+ return {"redacted": "Output data not logged for privacy/security"}
252
+
253
+ if hide_input:
254
+ input_processor = _default_input_processor
255
+ if hide_output:
256
+ output_processor = _default_output_processor
257
+
258
+ # Store the parameters for later reapplication
259
+ params = {
260
+ "name": name,
261
+ "run_type": run_type,
262
+ "span_type": span_type,
263
+ "input_processor": input_processor,
264
+ "output_processor": output_processor,
265
+ "recording": recording,
266
+ }
267
+
268
+ tracer_impl = _opentelemetry_traced
269
+
270
+ def decorator(func):
271
+ # Check which parameters are supported by the tracer_impl
272
+ supported_params = get_supported_params(tracer_impl, params)
273
+
274
+ # Decorate the function with only supported parameters
275
+ decorated_func = tracer_impl(**supported_params)(func)
276
+
277
+ return decorated_func
278
+
279
+ return decorator
280
+
281
+
282
+ __all__ = ["traced"]
@@ -0,0 +1,314 @@
1
+ """Tracing manager for handling tracer implementations and function registry."""
2
+
3
+ import logging
4
+ from typing import Callable, Dict, List, Optional
5
+
6
+ from opentelemetry import context, trace
7
+ from opentelemetry.trace import Span, set_span_in_context
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class SpanRegistry:
13
+ """Registry to track all spans and their parent relationships."""
14
+
15
+ MAX_HIERARCHY_DEPTH = 1000 # Hard limit for hierarchy traversal
16
+
17
+ def __init__(self):
18
+ self._spans: Dict[int, Span] = {} # span_id -> span
19
+ self._parent_map: Dict[int, Optional[int]] = {} # span_id -> parent_id
20
+
21
+ def register_span(self, span: Span) -> None:
22
+ """Register a span and its parent relationship."""
23
+ span_id = span.get_span_context().span_id
24
+
25
+ parent_id: Optional[int] = None
26
+
27
+ if hasattr(span, "parent") and span.parent is not None:
28
+ parent_id = getattr(span.parent, "span_id", None)
29
+
30
+ self._spans[span_id] = span
31
+ self._parent_map[span_id] = parent_id
32
+
33
+ parent_str = "{:016x}".format(parent_id) if parent_id is not None else "None"
34
+
35
+ logger.debug(
36
+ "SpanRegistry: registered span: %s (id: %016x, parent: %s)",
37
+ getattr(span, "name", "unknown"),
38
+ span_id,
39
+ parent_str,
40
+ )
41
+
42
+ def get_span(self, span_id: int) -> Optional[Span]:
43
+ """Get a span by ID."""
44
+ return self._spans.get(span_id)
45
+
46
+ def get_parent_id(self, span_id: int) -> Optional[int]:
47
+ """Get the parent ID of a span."""
48
+ return self._parent_map.get(span_id)
49
+
50
+ def calculate_depth(self, span_id: int) -> int:
51
+ """Calculate the depth of a span in the hierarchy.
52
+
53
+ Returns:
54
+ The depth of the span, capped at MAX_HIERARCHY_DEPTH.
55
+ """
56
+ depth = 0
57
+ current_id = span_id
58
+ visited = set()
59
+
60
+ while current_id is not None and current_id not in visited:
61
+ visited.add(current_id)
62
+ parent_id = self._parent_map.get(current_id)
63
+ if parent_id is None:
64
+ break
65
+ depth += 1
66
+ if depth >= self.MAX_HIERARCHY_DEPTH:
67
+ logger.warning(
68
+ "Hit MAX_HIERARCHY_DEPTH (%d) while calculating depth for span %016x",
69
+ self.MAX_HIERARCHY_DEPTH,
70
+ span_id,
71
+ )
72
+ break
73
+ current_id = parent_id
74
+
75
+ return depth
76
+
77
+ def is_ancestor(self, ancestor_id: int, descendant_id: int) -> bool:
78
+ """Check if ancestor_id is an ancestor of descendant_id.
79
+
80
+ Returns:
81
+ True if ancestor_id is an ancestor of descendant_id, False otherwise.
82
+ If MAX_HIERARCHY_DEPTH is reached, returns False.
83
+ """
84
+ current_id: Optional[int] = descendant_id
85
+ visited = set()
86
+ steps = 0
87
+
88
+ while current_id is not None and current_id not in visited:
89
+ if current_id == ancestor_id:
90
+ return True
91
+ visited.add(current_id)
92
+ current_id = self._parent_map.get(current_id)
93
+ steps += 1
94
+ if steps >= self.MAX_HIERARCHY_DEPTH:
95
+ logger.warning(
96
+ "Hit MAX_HIERARCHY_DEPTH (%d) while checking ancestry between %016x and %016x",
97
+ self.MAX_HIERARCHY_DEPTH,
98
+ ancestor_id,
99
+ descendant_id,
100
+ )
101
+ return False
102
+
103
+ return False
104
+
105
+ def clear(self) -> None:
106
+ """Clear all registered spans."""
107
+ self._spans.clear()
108
+ self._parent_map.clear()
109
+
110
+
111
+ # Global span registry instance
112
+ _span_registry = SpanRegistry()
113
+
114
+
115
+ class UiPathTracingManager:
116
+ """Static utility class to manage tracing implementations and decorated functions."""
117
+
118
+ _current_span_provider: Optional[Callable[[], Optional[Span]]] = None
119
+ _current_span_ancestors_provider: Optional[Callable[[], List[Span]]] = None
120
+
121
+ @staticmethod
122
+ def register_current_span_provider(
123
+ current_span_provider: Optional[Callable[[], Optional[Span]]],
124
+ ):
125
+ """Register a custom current span provider function.
126
+
127
+ Args:
128
+ current_span_provider: A function that returns the current span from an external
129
+ tracing framework. If None, no custom span parenting will be used.
130
+ """
131
+ UiPathTracingManager._current_span_provider = current_span_provider
132
+
133
+ @staticmethod
134
+ def get_parent_context() -> context.Context:
135
+ """Get the parent context for span creation.
136
+
137
+ This method determines the correct parent context when creating a new traced span.
138
+ It handles scenarios where spans may exist in both OpenTelemetry's context (current_span)
139
+ and in an external tracing system (external_span), such as LangGraph.
140
+
141
+ The algorithm follows this priority:
142
+
143
+ 1. **No spans available**: Returns the current OpenTelemetry context (empty context)
144
+
145
+ 2. **Only current_span exists**: Returns a context with current_span set as parent
146
+ - This is the standard OpenTelemetry behavior for nested traced functions
147
+
148
+ 3. **Only external_span exists**: Returns a context with external_span set as parent
149
+ - This occurs when an external tracing system (like LangGraph) has an active span
150
+ but there's no OTel span in the current call stack
151
+
152
+ 4. **Both spans exist**: Calls `_get_bottom_most_span()` to determine which is deeper
153
+ - Uses the SpanRegistry to build parent-child relationships
154
+ - Returns the span that is closer to the "bottom" (leaf) of the trace tree
155
+ - This ensures new spans are always attached to the deepest/most specific parent
156
+
157
+ Context Sources:
158
+ - **current_span**: Retrieved from OpenTelemetry's `trace.get_current_span()`
159
+ - Represents the active OTel span in the current execution context
160
+ - Created by `@traced` decorators or manual span creation
161
+
162
+ - **external_span**: Retrieved from the registered custom span provider
163
+ - Set via `register_current_span_provider()`
164
+ - Typically provided by external frameworks (LangGraph, LangChain, etc.)
165
+ - Allows integration with tracing systems outside of OpenTelemetry
166
+
167
+ Returns:
168
+ context.Context: An OpenTelemetry context containing the appropriate parent span,
169
+ or the current empty context if no spans are available
170
+
171
+ Example:
172
+ ```python
173
+ # Called by the @traced decorator when creating a new span:
174
+ ctx = UiPathTracingManager.get_parent_context()
175
+ with tracer.start_as_current_span("my_span", context=ctx) as span:
176
+ # New span will have the correct parent based on the logic above
177
+ pass
178
+ ```
179
+
180
+ See Also:
181
+ - `_get_bottom_most_span()`: Logic for choosing between two available spans
182
+ - `register_current_span_provider()`: Register external span provider
183
+ - `get_external_current_span()`: Retrieve span from external provider
184
+ """
185
+ current_span = trace.get_current_span()
186
+ has_current_span = (
187
+ current_span is not None and current_span.get_span_context().is_valid
188
+ )
189
+
190
+ external_span = UiPathTracingManager.get_external_current_span()
191
+
192
+ # Only one or no spans available
193
+ if not has_current_span:
194
+ return (
195
+ set_span_in_context(external_span)
196
+ if external_span is not None
197
+ else context.get_current()
198
+ )
199
+ if external_span is None:
200
+ return set_span_in_context(current_span)
201
+
202
+ # Both spans exist - find the bottom-most one
203
+ bottom_span = UiPathTracingManager._get_bottom_most_span(
204
+ current_span, external_span
205
+ )
206
+ return set_span_in_context(bottom_span)
207
+
208
+ @staticmethod
209
+ def _get_bottom_most_span(
210
+ current_span: Span,
211
+ external_span: Span,
212
+ ) -> Span:
213
+ """Determine which span is deeper in the ancestor tree.
214
+
215
+ Args:
216
+ current_span: The OTel current span
217
+ external_span: The external span from the provider
218
+
219
+ Returns:
220
+ The span that is deeper (closer to the bottom) in the call hierarchy
221
+ """
222
+ # Register both spans in the registry
223
+ _span_registry.register_span(current_span)
224
+ _span_registry.register_span(external_span)
225
+
226
+ # Also register external ancestors
227
+ external_ancestors = UiPathTracingManager.get_ancestor_spans() or []
228
+ for ancestor in external_ancestors:
229
+ _span_registry.register_span(ancestor)
230
+
231
+ current_span_id = current_span.get_span_context().span_id
232
+ external_span_id = external_span.get_span_context().span_id
233
+
234
+ # Check if one span is an ancestor of the other
235
+ if _span_registry.is_ancestor(external_span_id, current_span_id):
236
+ logger.debug(
237
+ "Traced Context: current_span is a descendant of external_span -> returning current_span (deeper)"
238
+ )
239
+ return current_span
240
+ elif _span_registry.is_ancestor(current_span_id, external_span_id):
241
+ logger.debug(
242
+ "Traced Context: external_span is a descendant of current_span -> returning external_span (deeper)"
243
+ )
244
+ return external_span
245
+
246
+ # Neither is an ancestor of the other - they're in different branches
247
+ # Use depth as tiebreaker
248
+ current_depth = _span_registry.calculate_depth(current_span_id)
249
+ external_depth = _span_registry.calculate_depth(external_span_id)
250
+
251
+ if current_depth > external_depth:
252
+ logger.debug(
253
+ "Traced Context: Different branches, current_span is deeper (depth %d > %d) -> returning current_span",
254
+ current_depth,
255
+ external_depth,
256
+ )
257
+ return current_span
258
+ elif external_depth > current_depth:
259
+ logger.debug(
260
+ "Traced Context: Different branches, external_span is deeper (depth %d > %d) -> returning external_span",
261
+ external_depth,
262
+ current_depth,
263
+ )
264
+ return external_span
265
+ else:
266
+ # Same depth, different branches - default to external
267
+ logger.debug(
268
+ "Traced Context: Same depth (%d), different branches -> defaulting to external_span",
269
+ current_depth,
270
+ )
271
+ return external_span
272
+
273
+ @staticmethod
274
+ def get_external_current_span() -> Optional[Span]:
275
+ """Get the current span from the external provider, if any."""
276
+ if UiPathTracingManager._current_span_provider is not None:
277
+ try:
278
+ return UiPathTracingManager._current_span_provider()
279
+ except Exception as e:
280
+ logger.warning("Error getting current span from provider: %s", e)
281
+ return None
282
+
283
+ @staticmethod
284
+ def get_ancestor_spans() -> List[Span]:
285
+ """Get the ancestor spans from the registered provider, if any."""
286
+ if UiPathTracingManager._current_span_ancestors_provider is not None:
287
+ try:
288
+ return UiPathTracingManager._current_span_ancestors_provider()
289
+ except Exception as e:
290
+ logger.warning("Error getting ancestor spans from provider: %s", e)
291
+ return []
292
+
293
+ @staticmethod
294
+ def register_current_span_ancestors_provider(
295
+ current_span_ancestors_provider: Optional[Callable[[], List[Span]]],
296
+ ):
297
+ """Register a custom current span ancestors provider function.
298
+
299
+ Args:
300
+ current_span_ancestors_provider: A function that returns a list of ancestor spans
301
+ from an external tracing framework. If None, no custom
302
+ span ancestor information will be used.
303
+ """
304
+ UiPathTracingManager._current_span_ancestors_provider = (
305
+ current_span_ancestors_provider
306
+ )
307
+
308
+ @staticmethod
309
+ def get_current_span_ancestors_provider():
310
+ """Get the currently set custom span ancestors provider."""
311
+ return UiPathTracingManager._current_span_ancestors_provider
312
+
313
+
314
+ __all__ = ["UiPathTracingManager"]
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: uipath-core
3
+ Version: 0.0.2
4
+ Summary: UiPath Core abstractions
5
+ Project-URL: Homepage, https://uipath.com
6
+ Project-URL: Repository, https://github.com/UiPath/uipath-runtime-python
7
+ Project-URL: Documentation, https://uipath.github.io/uipath-python/
8
+ Maintainer-email: Marius Cosareanu <marius.cosareanu@uipath.com>, Cristian Pufu <cristian.pufu@uipath.com>
9
+ License-File: LICENSE
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Software Development :: Build Tools
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: opentelemetry-instrumentation>=0.59b0
18
+ Requires-Dist: opentelemetry-sdk>=1.38.0
19
+ Requires-Dist: pydantic>=2.12.3
20
+ Description-Content-Type: text/markdown
21
+
22
+ # UiPath Core
23
+
24
+ Core abstractions and contracts for the UiPath Python SDK.
@@ -0,0 +1,11 @@
1
+ uipath/core/__init__.py,sha256=Gvk2xmI4JTOOHhQYtb0jJhSK2xmlrMUVTj7BL9lZQ88,196
2
+ uipath/core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ uipath/core/tracing/__init__.py,sha256=bY1t3KHAkyPZ1lYFigDFIvUdw4WCNEaackA0NyZr4IM,484
4
+ uipath/core/tracing/_utils.py,sha256=HNI07gEGjdxbSkfO9654JXszsIlN-iGRrSKdxfbQfAw,3994
5
+ uipath/core/tracing/context.py,sha256=jLUNGIRAydtXoi0W5Fs51SHEHSPHLkixXiupzz7zN88,634
6
+ uipath/core/tracing/decorators.py,sha256=bot1vlGcsaMre63TXLWCTrlnNitb9XE8Ynxz0w0z0mk,10721
7
+ uipath/core/tracing/manager.py,sha256=izxopU8Ar3VQCvBmaW_SD2d1VhUjLjID9YhVDT4Qbis,12367
8
+ uipath_core-0.0.2.dist-info/METADATA,sha256=B39O-uWDPumdj0uAiR_BLltGcshkJWu8jp6QQ-lOIlg,971
9
+ uipath_core-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ uipath_core-0.0.2.dist-info/licenses/LICENSE,sha256=-KBavWXepyDjimmzH5fVAsi-6jNVpIKFc2kZs0Ri4ng,1058
11
+ uipath_core-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright 2025 UiPath
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.