uipath 2.1.41__py3-none-any.whl → 2.1.43__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.
- uipath/_cli/_auth/_auth_server.py +2 -2
- uipath/_cli/_auth/_auth_service.py +151 -0
- uipath/_cli/_auth/_client_credentials.py +21 -22
- uipath/_cli/_auth/_oidc_utils.py +49 -44
- uipath/_cli/_auth/_portal_service.py +23 -22
- uipath/_cli/_auth/_url_utils.py +15 -5
- uipath/_cli/_dev/_terminal/__init__.py +9 -0
- uipath/_cli/_dev/_terminal/_components/_details.py +5 -1
- uipath/_cli/_evals/_models/_agent_execution_output.py +14 -0
- uipath/_cli/_evals/_runtime.py +172 -0
- uipath/_cli/_runtime/_contracts.py +135 -6
- uipath/_cli/_utils/_eval_set.py +84 -0
- uipath/_cli/cli_auth.py +18 -151
- uipath/_cli/cli_eval.py +73 -42
- uipath/_cli/cli_run.py +10 -36
- uipath/_cli/middlewares.py +1 -0
- uipath/_utils/constants.py +3 -0
- uipath/eval/_helpers/__init__.py +3 -0
- uipath/eval/_helpers/helpers.py +47 -0
- {uipath-2.1.41.dist-info → uipath-2.1.43.dist-info}/METADATA +1 -1
- {uipath-2.1.41.dist-info → uipath-2.1.43.dist-info}/RECORD +24 -19
- uipath/_cli/_evals/evaluation_service.py +0 -582
- {uipath-2.1.41.dist-info → uipath-2.1.43.dist-info}/WHEEL +0 -0
- {uipath-2.1.41.dist-info → uipath-2.1.43.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.41.dist-info → uipath-2.1.43.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,172 @@
|
|
1
|
+
import copy
|
2
|
+
from collections import defaultdict
|
3
|
+
from time import time
|
4
|
+
from typing import Dict, Generic, List, Optional, Sequence, TypeVar
|
5
|
+
|
6
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
7
|
+
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
8
|
+
|
9
|
+
from uipath.eval._helpers import auto_discover_entrypoint
|
10
|
+
|
11
|
+
from .._runtime._contracts import (
|
12
|
+
UiPathBaseRuntime,
|
13
|
+
UiPathRuntimeContext,
|
14
|
+
UiPathRuntimeFactory,
|
15
|
+
UiPathRuntimeResult,
|
16
|
+
UiPathRuntimeStatus,
|
17
|
+
)
|
18
|
+
from .._utils._eval_set import EvalHelpers
|
19
|
+
from ._models import EvaluationItem
|
20
|
+
from ._models._agent_execution_output import UiPathEvalRunExecutionOutput
|
21
|
+
|
22
|
+
T = TypeVar("T", bound=UiPathBaseRuntime)
|
23
|
+
C = TypeVar("C", bound=UiPathRuntimeContext)
|
24
|
+
|
25
|
+
|
26
|
+
class ExecutionSpanExporter(SpanExporter):
|
27
|
+
"""Custom exporter that stores spans grouped by execution ids."""
|
28
|
+
|
29
|
+
def __init__(self):
|
30
|
+
# { execution_id -> list of spans }
|
31
|
+
self._spans: Dict[str, List[ReadableSpan]] = defaultdict(list)
|
32
|
+
|
33
|
+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
34
|
+
for span in spans:
|
35
|
+
if span.attributes is not None:
|
36
|
+
exec_id = span.attributes.get("execution.id")
|
37
|
+
if exec_id is not None and isinstance(exec_id, str):
|
38
|
+
self._spans[exec_id].append(span)
|
39
|
+
|
40
|
+
return SpanExportResult.SUCCESS
|
41
|
+
|
42
|
+
def get_spans(self, execution_id: str) -> List[ReadableSpan]:
|
43
|
+
"""Retrieve spans for a given execution id."""
|
44
|
+
return self._spans.get(execution_id, [])
|
45
|
+
|
46
|
+
def clear(self, execution_id: Optional[str] = None) -> None:
|
47
|
+
"""Clear stored spans for one or all executions."""
|
48
|
+
if execution_id:
|
49
|
+
self._spans.pop(execution_id, None)
|
50
|
+
else:
|
51
|
+
self._spans.clear()
|
52
|
+
|
53
|
+
def shutdown(self) -> None:
|
54
|
+
self.clear()
|
55
|
+
|
56
|
+
|
57
|
+
class UiPathEvalContext(UiPathRuntimeContext, Generic[C]):
|
58
|
+
"""Context used for evaluation runs."""
|
59
|
+
|
60
|
+
runtime_context: C
|
61
|
+
no_report: bool
|
62
|
+
workers: int
|
63
|
+
eval_set: Optional[str] = None
|
64
|
+
eval_ids: Optional[List[str]] = None
|
65
|
+
|
66
|
+
def __init__(
|
67
|
+
self,
|
68
|
+
runtime_context: C,
|
69
|
+
no_report: bool,
|
70
|
+
workers: int,
|
71
|
+
eval_set: Optional[str] = None,
|
72
|
+
eval_ids: Optional[List[str]] = None,
|
73
|
+
**kwargs,
|
74
|
+
):
|
75
|
+
super().__init__(
|
76
|
+
runtime_context=runtime_context, # type: ignore
|
77
|
+
no_report=no_report,
|
78
|
+
workers=workers,
|
79
|
+
eval_set=eval_set,
|
80
|
+
eval_ids=eval_ids,
|
81
|
+
**kwargs,
|
82
|
+
)
|
83
|
+
self._auto_discover()
|
84
|
+
|
85
|
+
def _auto_discover(self):
|
86
|
+
self.runtime_context.entrypoint = (
|
87
|
+
self.runtime_context.entrypoint or auto_discover_entrypoint()
|
88
|
+
)
|
89
|
+
self.eval_set = self.eval_set or EvalHelpers.auto_discover_eval_set()
|
90
|
+
|
91
|
+
|
92
|
+
class UiPathEvalRuntime(UiPathBaseRuntime, Generic[T, C]):
|
93
|
+
"""Specialized runtime for evaluation runs, with access to the factory."""
|
94
|
+
|
95
|
+
def __init__(
|
96
|
+
self, context: "UiPathEvalContext[C]", factory: "UiPathRuntimeFactory[T, C]"
|
97
|
+
):
|
98
|
+
super().__init__(context)
|
99
|
+
self.context: "UiPathEvalContext[C]" = context
|
100
|
+
self.factory: UiPathRuntimeFactory[T, C] = factory
|
101
|
+
self.span_exporter: ExecutionSpanExporter = ExecutionSpanExporter()
|
102
|
+
self.factory.add_span_exporter(self.span_exporter)
|
103
|
+
|
104
|
+
@classmethod
|
105
|
+
def from__eval_context(
|
106
|
+
cls,
|
107
|
+
context: "UiPathEvalContext[C]",
|
108
|
+
factory: "UiPathRuntimeFactory[T, C]",
|
109
|
+
) -> "UiPathEvalRuntime[T, C]":
|
110
|
+
return cls(context, factory)
|
111
|
+
|
112
|
+
async def execute(self) -> Optional[UiPathRuntimeResult]:
|
113
|
+
"""Evaluation logic. Can spawn other runtimes through the factory."""
|
114
|
+
if self.context.eval_set is None:
|
115
|
+
raise ValueError("eval_set must be provided for evaluation runs")
|
116
|
+
|
117
|
+
evaluation_set = EvalHelpers.load_eval_set(
|
118
|
+
self.context.eval_set, self.context.eval_ids
|
119
|
+
)
|
120
|
+
execution_output_list: list[UiPathEvalRunExecutionOutput] = []
|
121
|
+
for eval_item in evaluation_set.evaluations:
|
122
|
+
execution_output_list.append(await self.execute_agent(eval_item))
|
123
|
+
|
124
|
+
self.context.result = UiPathRuntimeResult(
|
125
|
+
output={
|
126
|
+
"results": execution_output_list,
|
127
|
+
},
|
128
|
+
status=UiPathRuntimeStatus.SUCCESSFUL,
|
129
|
+
resume=None,
|
130
|
+
)
|
131
|
+
|
132
|
+
return self.context.runtime_context.result
|
133
|
+
|
134
|
+
def _prepare_new_runtime_context(self, eval_item: EvaluationItem) -> C:
|
135
|
+
runtime_context = copy.deepcopy(self.context.runtime_context)
|
136
|
+
runtime_context.execution_id = eval_item.id
|
137
|
+
runtime_context.input_json = eval_item.inputs
|
138
|
+
# here we can pass other values from eval_item: expectedAgentBehavior, simulationInstructions etc.
|
139
|
+
return runtime_context
|
140
|
+
|
141
|
+
# TODO: this would most likely need to be ported to a public AgentEvaluator class
|
142
|
+
async def execute_agent(
|
143
|
+
self, eval_item: EvaluationItem
|
144
|
+
) -> "UiPathEvalRunExecutionOutput":
|
145
|
+
runtime_context = self._prepare_new_runtime_context(eval_item)
|
146
|
+
start_time = time()
|
147
|
+
result = await self.factory.execute_in_root_span(
|
148
|
+
runtime_context, root_span=eval_item.name
|
149
|
+
)
|
150
|
+
end_time = time()
|
151
|
+
if runtime_context.execution_id is None:
|
152
|
+
raise ValueError("execution_id must be set for eval runs")
|
153
|
+
|
154
|
+
spans = self.span_exporter.get_spans(runtime_context.execution_id)
|
155
|
+
self.span_exporter.clear(runtime_context.execution_id)
|
156
|
+
|
157
|
+
if result is None:
|
158
|
+
raise ValueError("Execution result cannot be None for eval runs")
|
159
|
+
|
160
|
+
return UiPathEvalRunExecutionOutput(
|
161
|
+
execution_time=end_time - start_time,
|
162
|
+
spans=spans,
|
163
|
+
result=result,
|
164
|
+
)
|
165
|
+
|
166
|
+
async def cleanup(self) -> None:
|
167
|
+
"""Cleanup runtime resources."""
|
168
|
+
pass
|
169
|
+
|
170
|
+
async def validate(self) -> None:
|
171
|
+
"""Cleanup runtime resources."""
|
172
|
+
pass
|
@@ -9,6 +9,7 @@ from abc import ABC, abstractmethod
|
|
9
9
|
from enum import Enum
|
10
10
|
from functools import cached_property
|
11
11
|
from typing import Any, Callable, Dict, Generic, List, Optional, Type, TypeVar, Union
|
12
|
+
from uuid import uuid4
|
12
13
|
|
13
14
|
from opentelemetry import context as context_api
|
14
15
|
from opentelemetry import trace
|
@@ -23,7 +24,7 @@ from opentelemetry.trace import Tracer
|
|
23
24
|
from pydantic import BaseModel, Field
|
24
25
|
|
25
26
|
from uipath.agent.conversation import UiPathConversationEvent, UiPathConversationMessage
|
26
|
-
from uipath.tracing import TracingManager
|
27
|
+
from uipath.tracing import LlmOpsHttpExporter, TracingManager
|
27
28
|
|
28
29
|
from ._logging import LogsInterceptor
|
29
30
|
|
@@ -161,6 +162,128 @@ class UiPathTraceContext(BaseModel):
|
|
161
162
|
reference_id: Optional[str] = None
|
162
163
|
|
163
164
|
|
165
|
+
class UiPathRuntimeContextBuilder:
|
166
|
+
"""Builder class for UiPathRuntimeContext following the builder pattern."""
|
167
|
+
|
168
|
+
def __init__(self):
|
169
|
+
self._kwargs = {}
|
170
|
+
|
171
|
+
def with_defaults(
|
172
|
+
self, config_path: Optional[str] = None, **kwargs
|
173
|
+
) -> "UiPathRuntimeContextBuilder":
|
174
|
+
"""Apply default configuration similar to UiPathRuntimeContext.with_defaults().
|
175
|
+
|
176
|
+
Args:
|
177
|
+
config_path: Path to the configuration file (defaults to UIPATH_CONFIG_PATH env var or "uipath.json")
|
178
|
+
**kwargs: Additional keyword arguments to pass to with_defaults
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
Self for method chaining
|
182
|
+
"""
|
183
|
+
from os import environ as env
|
184
|
+
|
185
|
+
resolved_config_path = config_path or env.get(
|
186
|
+
"UIPATH_CONFIG_PATH", "uipath.json"
|
187
|
+
)
|
188
|
+
self._kwargs["config_path"] = resolved_config_path
|
189
|
+
|
190
|
+
self._kwargs.update(
|
191
|
+
{
|
192
|
+
"job_id": env.get("UIPATH_JOB_KEY"),
|
193
|
+
"trace_id": env.get("UIPATH_TRACE_ID"),
|
194
|
+
"tracing_enabled": env.get("UIPATH_TRACING_ENABLED", True),
|
195
|
+
"logs_min_level": env.get("LOG_LEVEL", "INFO"),
|
196
|
+
**kwargs, # Allow overriding defaults with provided kwargs
|
197
|
+
}
|
198
|
+
)
|
199
|
+
|
200
|
+
self._kwargs["trace_context"] = UiPathTraceContext(
|
201
|
+
trace_id=env.get("UIPATH_TRACE_ID"),
|
202
|
+
parent_span_id=env.get("UIPATH_PARENT_SPAN_ID"),
|
203
|
+
root_span_id=env.get("UIPATH_ROOT_SPAN_ID"),
|
204
|
+
enabled=env.get("UIPATH_TRACING_ENABLED", True),
|
205
|
+
job_id=env.get("UIPATH_JOB_KEY"),
|
206
|
+
org_id=env.get("UIPATH_ORGANIZATION_ID"),
|
207
|
+
tenant_id=env.get("UIPATH_TENANT_ID"),
|
208
|
+
process_key=env.get("UIPATH_PROCESS_UUID"),
|
209
|
+
folder_key=env.get("UIPATH_FOLDER_KEY"),
|
210
|
+
reference_id=env.get("UIPATH_JOB_KEY") or str(uuid4()),
|
211
|
+
)
|
212
|
+
|
213
|
+
return self
|
214
|
+
|
215
|
+
def with_entrypoint(self, entrypoint: str) -> "UiPathRuntimeContextBuilder":
|
216
|
+
"""Set the entrypoint for the runtime context.
|
217
|
+
|
218
|
+
Args:
|
219
|
+
entrypoint: The entrypoint to execute
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
Self for method chaining
|
223
|
+
"""
|
224
|
+
self._kwargs["entrypoint"] = entrypoint
|
225
|
+
return self
|
226
|
+
|
227
|
+
def with_input(
|
228
|
+
self, input_data: Optional[str] = None, input_file: Optional[str] = None
|
229
|
+
) -> "UiPathRuntimeContextBuilder":
|
230
|
+
"""Set the input data for the runtime context.
|
231
|
+
|
232
|
+
Args:
|
233
|
+
input_data: The input data as a string
|
234
|
+
input_file: Path to the input file
|
235
|
+
|
236
|
+
Returns:
|
237
|
+
Self for method chaining
|
238
|
+
"""
|
239
|
+
if input_data is not None:
|
240
|
+
self._kwargs["input"] = input_data
|
241
|
+
if input_file is not None:
|
242
|
+
self._kwargs["input_file"] = input_file
|
243
|
+
return self
|
244
|
+
|
245
|
+
def with_resume(self, enable: bool = True) -> "UiPathRuntimeContextBuilder":
|
246
|
+
"""Enable or disable resume mode for the runtime context.
|
247
|
+
|
248
|
+
Args:
|
249
|
+
enable: Whether to enable resume mode (defaults to True)
|
250
|
+
|
251
|
+
Returns:
|
252
|
+
Self for method chaining
|
253
|
+
"""
|
254
|
+
self._kwargs["resume"] = enable
|
255
|
+
return self
|
256
|
+
|
257
|
+
def mark_eval_run(self, enable: bool = True) -> "UiPathRuntimeContextBuilder":
|
258
|
+
"""Mark this as an evaluation run.
|
259
|
+
|
260
|
+
Args:
|
261
|
+
enable: Whether this is an eval run (defaults to True)
|
262
|
+
|
263
|
+
Returns:
|
264
|
+
Self for method chaining
|
265
|
+
"""
|
266
|
+
self._kwargs["is_eval_run"] = enable
|
267
|
+
return self
|
268
|
+
|
269
|
+
def build(self) -> "UiPathRuntimeContext":
|
270
|
+
"""Build and return the UiPathRuntimeContext instance.
|
271
|
+
|
272
|
+
Returns:
|
273
|
+
A configured UiPathRuntimeContext instance
|
274
|
+
"""
|
275
|
+
config_path = self._kwargs.pop("config_path", None)
|
276
|
+
if config_path:
|
277
|
+
# Create context from config first, then update with any additional kwargs
|
278
|
+
context = UiPathRuntimeContext.from_config(config_path)
|
279
|
+
for key, value in self._kwargs.items():
|
280
|
+
if hasattr(context, key):
|
281
|
+
setattr(context, key, value)
|
282
|
+
return context
|
283
|
+
else:
|
284
|
+
return UiPathRuntimeContext(**self._kwargs)
|
285
|
+
|
286
|
+
|
164
287
|
class UiPathRuntimeContext(BaseModel):
|
165
288
|
"""Context information passed throughout the runtime execution."""
|
166
289
|
|
@@ -422,8 +545,8 @@ class UiPathBaseRuntime(ABC):
|
|
422
545
|
content = execution_result.to_dict()
|
423
546
|
logger.debug(content)
|
424
547
|
|
425
|
-
# Always write output file at runtime
|
426
|
-
if self.context.job_id:
|
548
|
+
# Always write output file at runtime, except evaluation runs
|
549
|
+
if self.context.job_id and not self.context.is_eval_run:
|
427
550
|
with open(self.output_file_path, "w") as f:
|
428
551
|
json.dump(content, f, indent=2, default=str)
|
429
552
|
|
@@ -521,6 +644,9 @@ class UiPathRuntimeFactory(Generic[T, C]):
|
|
521
644
|
self.tracer_span_processors: List[SpanProcessor] = []
|
522
645
|
trace.set_tracer_provider(self.tracer_provider)
|
523
646
|
|
647
|
+
if os.getenv("UIPATH_JOB_KEY"):
|
648
|
+
self.add_span_exporter(LlmOpsHttpExporter())
|
649
|
+
|
524
650
|
def add_span_exporter(
|
525
651
|
self,
|
526
652
|
span_exporter: SpanExporter,
|
@@ -591,9 +717,12 @@ class UiPathExecutionTraceProcessorMixin:
|
|
591
717
|
parent_span = trace.get_current_span()
|
592
718
|
|
593
719
|
if parent_span and parent_span.is_recording():
|
594
|
-
|
595
|
-
if
|
596
|
-
span.set_attribute("execution.id",
|
720
|
+
execution_id = parent_span.attributes.get("execution.id") # type: ignore[attr-defined]
|
721
|
+
if execution_id:
|
722
|
+
span.set_attribute("execution.id", execution_id)
|
723
|
+
evaluation_id = parent_span.attributes.get("evaluation.id") # type: ignore[attr-defined]
|
724
|
+
if evaluation_id:
|
725
|
+
span.set_attribute("evaluation.id", evaluation_id)
|
597
726
|
|
598
727
|
|
599
728
|
class UiPathExecutionBatchTraceProcessor(
|
@@ -0,0 +1,84 @@
|
|
1
|
+
import json
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import List, Optional
|
4
|
+
|
5
|
+
import click
|
6
|
+
|
7
|
+
from uipath._cli._evals._models import EvaluationSet
|
8
|
+
from uipath._cli._utils._console import ConsoleLogger
|
9
|
+
|
10
|
+
console = ConsoleLogger()
|
11
|
+
|
12
|
+
|
13
|
+
class EvalHelpers:
|
14
|
+
@staticmethod
|
15
|
+
def auto_discover_eval_set() -> str:
|
16
|
+
"""Auto-discover evaluation set from evals/eval-sets directory.
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
Path to the evaluation set file
|
20
|
+
|
21
|
+
Raises:
|
22
|
+
ValueError: If no eval set found or multiple eval sets exist
|
23
|
+
"""
|
24
|
+
eval_sets_dir = Path("evals/eval-sets")
|
25
|
+
|
26
|
+
if not eval_sets_dir.exists():
|
27
|
+
raise ValueError(
|
28
|
+
"No 'evals/eval-sets' directory found. "
|
29
|
+
"Please set 'UIPATH_PROJECT_ID' env var and run 'uipath pull'."
|
30
|
+
)
|
31
|
+
|
32
|
+
eval_set_files = list(eval_sets_dir.glob("*.json"))
|
33
|
+
|
34
|
+
if not eval_set_files:
|
35
|
+
raise ValueError(
|
36
|
+
"No evaluation set files found in 'evals/eval-sets' directory. "
|
37
|
+
)
|
38
|
+
|
39
|
+
if len(eval_set_files) > 1:
|
40
|
+
file_names = [f.name for f in eval_set_files]
|
41
|
+
raise ValueError(
|
42
|
+
f"Multiple evaluation sets found: {file_names}. "
|
43
|
+
f"Please specify which evaluation set to use: 'uipath eval [entrypoint] <eval_set_path>'"
|
44
|
+
)
|
45
|
+
|
46
|
+
eval_set_path = str(eval_set_files[0])
|
47
|
+
console.info(
|
48
|
+
f"Auto-discovered evaluation set: {click.style(eval_set_path, fg='cyan')}"
|
49
|
+
)
|
50
|
+
|
51
|
+
eval_set_path_obj = Path(eval_set_path)
|
52
|
+
if not eval_set_path_obj.is_file() or eval_set_path_obj.suffix != ".json":
|
53
|
+
raise ValueError("Evaluation set must be a JSON file")
|
54
|
+
|
55
|
+
return eval_set_path
|
56
|
+
|
57
|
+
@staticmethod
|
58
|
+
def load_eval_set(
|
59
|
+
eval_set_path: str, eval_ids: Optional[List[str]] = None
|
60
|
+
) -> EvaluationSet:
|
61
|
+
"""Load the evaluation set from file.
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
The loaded evaluation set as EvaluationSet model
|
65
|
+
"""
|
66
|
+
try:
|
67
|
+
with open(eval_set_path, "r", encoding="utf-8") as f:
|
68
|
+
data = json.load(f)
|
69
|
+
except json.JSONDecodeError as e:
|
70
|
+
raise ValueError(
|
71
|
+
f"Invalid JSON in evaluation set file '{eval_set_path}': {str(e)}. "
|
72
|
+
f"Please check the file for syntax errors."
|
73
|
+
) from e
|
74
|
+
|
75
|
+
try:
|
76
|
+
eval_set = EvaluationSet(**data)
|
77
|
+
except (TypeError, ValueError) as e:
|
78
|
+
raise ValueError(
|
79
|
+
f"Invalid evaluation set format in '{eval_set_path}': {str(e)}. "
|
80
|
+
f"Please verify the evaluation set structure."
|
81
|
+
) from e
|
82
|
+
if eval_ids:
|
83
|
+
eval_set.extract_selected_evals(eval_ids)
|
84
|
+
return eval_set
|
uipath/_cli/cli_auth.py
CHANGED
@@ -1,61 +1,15 @@
|
|
1
|
-
import asyncio
|
2
|
-
import json
|
3
|
-
import os
|
4
|
-
import socket
|
5
|
-
import webbrowser
|
6
1
|
from typing import Optional
|
7
|
-
from urllib.parse import urlparse
|
8
2
|
|
9
3
|
import click
|
10
4
|
|
11
5
|
from ..telemetry import track
|
12
|
-
from ._auth.
|
13
|
-
from ._auth._client_credentials import ClientCredentialsService
|
14
|
-
from ._auth._oidc_utils import get_auth_config, get_auth_url
|
15
|
-
from ._auth._portal_service import PortalService, select_tenant
|
16
|
-
from ._auth._utils import update_auth_file, update_env_file
|
6
|
+
from ._auth._auth_service import AuthService
|
17
7
|
from ._utils._common import environment_options
|
18
8
|
from ._utils._console import ConsoleLogger
|
19
9
|
|
20
10
|
console = ConsoleLogger()
|
21
11
|
|
22
12
|
|
23
|
-
def is_port_in_use(port: int) -> bool:
|
24
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
25
|
-
try:
|
26
|
-
s.bind(("localhost", port))
|
27
|
-
s.close()
|
28
|
-
return False
|
29
|
-
except socket.error:
|
30
|
-
return True
|
31
|
-
|
32
|
-
|
33
|
-
def set_port():
|
34
|
-
auth_config = get_auth_config()
|
35
|
-
port = int(auth_config.get("port", 8104))
|
36
|
-
port_option_one = int(auth_config.get("portOptionOne", 8104)) # type: ignore
|
37
|
-
port_option_two = int(auth_config.get("portOptionTwo", 8055)) # type: ignore
|
38
|
-
port_option_three = int(auth_config.get("portOptionThree", 42042)) # type: ignore
|
39
|
-
if is_port_in_use(port):
|
40
|
-
if is_port_in_use(port_option_one):
|
41
|
-
if is_port_in_use(port_option_two):
|
42
|
-
if is_port_in_use(port_option_three):
|
43
|
-
console.error(
|
44
|
-
"All configured ports are in use. Please close applications using ports or configure different ports."
|
45
|
-
)
|
46
|
-
else:
|
47
|
-
port = port_option_three
|
48
|
-
else:
|
49
|
-
port = port_option_two
|
50
|
-
else:
|
51
|
-
port = port_option_one
|
52
|
-
auth_config["port"] = port
|
53
|
-
with open(
|
54
|
-
os.path.join(os.path.dirname(__file__), "..", "auth_config.json"), "w"
|
55
|
-
) as f:
|
56
|
-
json.dump(auth_config, f)
|
57
|
-
|
58
|
-
|
59
13
|
@click.command()
|
60
14
|
@environment_options
|
61
15
|
@click.option(
|
@@ -63,6 +17,7 @@ def set_port():
|
|
63
17
|
"--force",
|
64
18
|
is_flag=True,
|
65
19
|
required=False,
|
20
|
+
default=False,
|
66
21
|
help="Force new token",
|
67
22
|
)
|
68
23
|
@click.option(
|
@@ -89,7 +44,7 @@ def set_port():
|
|
89
44
|
@track
|
90
45
|
def auth(
|
91
46
|
domain,
|
92
|
-
force:
|
47
|
+
force: bool = False,
|
93
48
|
client_id: Optional[str] = None,
|
94
49
|
client_secret: Optional[str] = None,
|
95
50
|
base_url: Optional[str] = None,
|
@@ -108,109 +63,21 @@ def auth(
|
|
108
63
|
- Set REQUESTS_CA_BUNDLE to specify a custom CA bundle for SSL verification
|
109
64
|
- Set UIPATH_DISABLE_SSL_VERIFY to disable SSL verification (not recommended)
|
110
65
|
"""
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
66
|
+
auth_service = AuthService(
|
67
|
+
domain,
|
68
|
+
force=force,
|
69
|
+
client_id=client_id,
|
70
|
+
client_secret=client_secret,
|
71
|
+
base_url=base_url,
|
72
|
+
scope=scope,
|
73
|
+
)
|
74
|
+
with console.spinner("Authenticating with UiPath ..."):
|
75
|
+
try:
|
76
|
+
auth_service.authenticate()
|
77
|
+
console.success(
|
78
|
+
"Authentication successful.",
|
119
79
|
)
|
120
|
-
|
121
|
-
|
122
|
-
# Check if client credentials are provided for unattended authentication
|
123
|
-
if client_id and client_secret:
|
124
|
-
if not base_url:
|
80
|
+
except KeyboardInterrupt:
|
125
81
|
console.error(
|
126
|
-
"
|
127
|
-
)
|
128
|
-
return
|
129
|
-
|
130
|
-
with console.spinner("Authenticating with client credentials ..."):
|
131
|
-
credentials_service = ClientCredentialsService(domain)
|
132
|
-
|
133
|
-
# If base_url is provided, extract domain from it to override the CLI domain parameter
|
134
|
-
if base_url:
|
135
|
-
extracted_domain = credentials_service.extract_domain_from_base_url(
|
136
|
-
base_url
|
137
|
-
)
|
138
|
-
credentials_service.domain = extracted_domain
|
139
|
-
|
140
|
-
token_data = credentials_service.authenticate(
|
141
|
-
client_id, client_secret, scope
|
142
|
-
)
|
143
|
-
|
144
|
-
if token_data:
|
145
|
-
credentials_service.setup_environment(token_data, base_url)
|
146
|
-
console.success(
|
147
|
-
"Client credentials authentication successful.",
|
148
|
-
)
|
149
|
-
else:
|
150
|
-
console.error(
|
151
|
-
"Client credentials authentication failed. Please check your credentials.",
|
152
|
-
)
|
153
|
-
return
|
154
|
-
|
155
|
-
# Interactive authentication flow (existing logic)
|
156
|
-
with console.spinner("Authenticating with UiPath ..."):
|
157
|
-
with PortalService(domain) as portal_service:
|
158
|
-
if not force:
|
159
|
-
if (
|
160
|
-
os.getenv("UIPATH_URL")
|
161
|
-
and os.getenv("UIPATH_TENANT_ID")
|
162
|
-
and os.getenv("UIPATH_ORGANIZATION_ID")
|
163
|
-
):
|
164
|
-
try:
|
165
|
-
portal_service.ensure_valid_token()
|
166
|
-
console.success(
|
167
|
-
"Authentication successful.",
|
168
|
-
)
|
169
|
-
return
|
170
|
-
except Exception:
|
171
|
-
console.info(
|
172
|
-
"Authentication token is invalid. Please reauthenticate.",
|
173
|
-
)
|
174
|
-
|
175
|
-
auth_url, code_verifier, state = get_auth_url(domain)
|
176
|
-
|
177
|
-
webbrowser.open(auth_url, 1)
|
178
|
-
auth_config = get_auth_config()
|
179
|
-
|
180
|
-
console.link(
|
181
|
-
"If a browser window did not open, please open the following URL in your browser:",
|
182
|
-
auth_url,
|
82
|
+
"Authentication cancelled by user.",
|
183
83
|
)
|
184
|
-
|
185
|
-
try:
|
186
|
-
server = HTTPServer(port=auth_config["port"])
|
187
|
-
token_data = asyncio.run(server.start(state, code_verifier, domain))
|
188
|
-
|
189
|
-
if not token_data:
|
190
|
-
console.error(
|
191
|
-
"Authentication failed. Please try again.",
|
192
|
-
)
|
193
|
-
return
|
194
|
-
|
195
|
-
portal_service.update_token_data(token_data)
|
196
|
-
update_auth_file(token_data)
|
197
|
-
access_token = token_data["access_token"]
|
198
|
-
update_env_file({"UIPATH_ACCESS_TOKEN": access_token})
|
199
|
-
|
200
|
-
tenants_and_organizations = (
|
201
|
-
portal_service.get_tenants_and_organizations()
|
202
|
-
)
|
203
|
-
base_url = select_tenant(domain, tenants_and_organizations)
|
204
|
-
try:
|
205
|
-
portal_service.post_auth(base_url)
|
206
|
-
console.success(
|
207
|
-
"Authentication successful.",
|
208
|
-
)
|
209
|
-
except Exception:
|
210
|
-
console.error(
|
211
|
-
"Could not prepare the environment. Please try again.",
|
212
|
-
)
|
213
|
-
except KeyboardInterrupt:
|
214
|
-
console.error(
|
215
|
-
"Authentication cancelled by user.",
|
216
|
-
)
|