uipath 2.1.131__py3-none-any.whl → 2.1.133__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.
Potentially problematic release.
This version of uipath might be problematic. Click here for more details.
- uipath/_cli/__init__.py +45 -3
- uipath/_cli/_runtime/_contracts.py +12 -10
- uipath/_cli/_utils/_context.py +65 -0
- uipath/_cli/_utils/_formatters.py +173 -0
- uipath/_cli/_utils/_service_base.py +340 -0
- uipath/_cli/_utils/_service_cli_generator.py +705 -0
- uipath/_cli/_utils/_service_metadata.py +218 -0
- uipath/_cli/_utils/_service_protocol.py +223 -0
- uipath/_cli/_utils/_type_registry.py +106 -0
- uipath/_cli/_utils/_validators.py +127 -0
- uipath/_cli/services/__init__.py +38 -0
- uipath/_cli/services/_buckets_metadata.py +53 -0
- uipath/_cli/services/cli_buckets.py +526 -0
- uipath/_resources/CLI_REFERENCE.md +340 -0
- uipath/_resources/SDK_REFERENCE.md +14 -2
- uipath/_services/buckets_service.py +169 -6
- uipath/utils/_endpoints_manager.py +184 -196
- {uipath-2.1.131.dist-info → uipath-2.1.133.dist-info}/METADATA +1 -1
- {uipath-2.1.131.dist-info → uipath-2.1.133.dist-info}/RECORD +22 -11
- {uipath-2.1.131.dist-info → uipath-2.1.133.dist-info}/WHEEL +0 -0
- {uipath-2.1.131.dist-info → uipath-2.1.133.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.131.dist-info → uipath-2.1.133.dist-info}/licenses/LICENSE +0 -0
uipath/_cli/__init__.py
CHANGED
|
@@ -4,6 +4,7 @@ import sys
|
|
|
4
4
|
import click
|
|
5
5
|
|
|
6
6
|
from ._utils._common import add_cwd_to_path, load_environment_variables
|
|
7
|
+
from ._utils._context import CliContext
|
|
7
8
|
from .cli_add import add as add
|
|
8
9
|
from .cli_auth import auth as auth
|
|
9
10
|
from .cli_debug import debug as debug # type: ignore
|
|
@@ -20,6 +21,9 @@ from .cli_push import push as push # type: ignore
|
|
|
20
21
|
from .cli_register import register as register # type: ignore
|
|
21
22
|
from .cli_run import run as run # type: ignore
|
|
22
23
|
|
|
24
|
+
load_environment_variables()
|
|
25
|
+
add_cwd_to_path()
|
|
26
|
+
|
|
23
27
|
|
|
24
28
|
def _get_safe_version() -> str:
|
|
25
29
|
"""Get the version of the uipath package."""
|
|
@@ -46,9 +50,39 @@ def _get_safe_version() -> str:
|
|
|
46
50
|
is_flag=True,
|
|
47
51
|
help="Display the current version of uipath.",
|
|
48
52
|
)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
@click.option(
|
|
54
|
+
"--format",
|
|
55
|
+
type=click.Choice(["json", "table", "csv"]),
|
|
56
|
+
default="table",
|
|
57
|
+
help="Output format for commands",
|
|
58
|
+
)
|
|
59
|
+
@click.option(
|
|
60
|
+
"--debug",
|
|
61
|
+
is_flag=True,
|
|
62
|
+
help="Enable debug logging and show stack traces",
|
|
63
|
+
)
|
|
64
|
+
@click.pass_context
|
|
65
|
+
def cli(
|
|
66
|
+
ctx: click.Context,
|
|
67
|
+
lv: bool,
|
|
68
|
+
v: bool,
|
|
69
|
+
format: str,
|
|
70
|
+
debug: bool,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""UiPath CLI - Automate everything.
|
|
73
|
+
|
|
74
|
+
\b
|
|
75
|
+
Examples:
|
|
76
|
+
uipath new my-project
|
|
77
|
+
uipath dev
|
|
78
|
+
uipath deploy
|
|
79
|
+
uipath buckets list --folder-path "Shared"
|
|
80
|
+
""" # noqa: D301
|
|
81
|
+
ctx.obj = CliContext(
|
|
82
|
+
output_format=format,
|
|
83
|
+
debug=debug,
|
|
84
|
+
)
|
|
85
|
+
|
|
52
86
|
if lv:
|
|
53
87
|
try:
|
|
54
88
|
version = importlib.metadata.version("uipath-langchain")
|
|
@@ -64,6 +98,10 @@ def cli(lv: bool, v: bool) -> None:
|
|
|
64
98
|
click.echo("uipath is not installed", err=True)
|
|
65
99
|
sys.exit(1)
|
|
66
100
|
|
|
101
|
+
# Show help if no command was provided (matches docker, kubectl, git behavior)
|
|
102
|
+
if ctx.invoked_subcommand is None and not lv and not v:
|
|
103
|
+
click.echo(ctx.get_help())
|
|
104
|
+
|
|
67
105
|
|
|
68
106
|
cli.add_command(new)
|
|
69
107
|
cli.add_command(init)
|
|
@@ -80,3 +118,7 @@ cli.add_command(dev)
|
|
|
80
118
|
cli.add_command(add)
|
|
81
119
|
cli.add_command(register)
|
|
82
120
|
cli.add_command(debug)
|
|
121
|
+
|
|
122
|
+
from .services import register_service_commands # noqa: E402
|
|
123
|
+
|
|
124
|
+
register_service_commands(cli)
|
|
@@ -384,6 +384,7 @@ class UiPathRuntimeContext(BaseModel):
|
|
|
384
384
|
chat_handler: Optional[UiPathConversationHandler] = None
|
|
385
385
|
is_conversational: Optional[bool] = None
|
|
386
386
|
breakpoints: Optional[List[str] | Literal["*"]] = None
|
|
387
|
+
intercept_logs: bool = True
|
|
387
388
|
|
|
388
389
|
model_config = {"arbitrary_types_allowed": True, "extra": "allow"}
|
|
389
390
|
|
|
@@ -631,16 +632,17 @@ class UiPathBaseRuntime(ABC):
|
|
|
631
632
|
|
|
632
633
|
# Intercept all stdout/stderr/logs
|
|
633
634
|
# write to file (runtime) or stdout (debug)
|
|
634
|
-
self.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
635
|
+
if self.context.intercept_logs:
|
|
636
|
+
self.logs_interceptor = LogsInterceptor(
|
|
637
|
+
min_level=self.context.logs_min_level,
|
|
638
|
+
dir=self.context.runtime_dir,
|
|
639
|
+
file=self.context.logs_file,
|
|
640
|
+
job_id=self.context.job_id,
|
|
641
|
+
execution_id=self.context.execution_id,
|
|
642
|
+
is_debug_run=self.is_debug_run(),
|
|
643
|
+
log_handler=self.context.log_handler,
|
|
644
|
+
)
|
|
645
|
+
self.logs_interceptor.setup()
|
|
644
646
|
|
|
645
647
|
logger.debug(f"Starting runtime with job id: {self.context.job_id}")
|
|
646
648
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Type-safe context management for Click commands.
|
|
2
|
+
|
|
3
|
+
This module provides type-safe access to CLI context across all commands,
|
|
4
|
+
improving developer experience and enabling better IDE autocomplete.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CliContext:
|
|
15
|
+
"""CLI global context object.
|
|
16
|
+
|
|
17
|
+
This provides type-safe access to configuration shared across all commands.
|
|
18
|
+
Using a dataclass ensures all attributes are properly typed and documented.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
output_format: Output format (table, json, csv)
|
|
22
|
+
debug: Enable debug logging
|
|
23
|
+
|
|
24
|
+
Note:
|
|
25
|
+
Authentication (URL and secret) are always read from environment variables
|
|
26
|
+
(UIPATH_URL and UIPATH_ACCESS_TOKEN).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
output_format: str = "table"
|
|
30
|
+
debug: bool = False
|
|
31
|
+
|
|
32
|
+
_client: Any = field(default=None, init=False, repr=False)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_cli_context(ctx: click.Context) -> CliContext:
|
|
36
|
+
"""Type-safe helper to retrieve CliContext from Click context.
|
|
37
|
+
|
|
38
|
+
This eliminates repeated cast() calls and provides autocompletion
|
|
39
|
+
for CLI context attributes.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
ctx: Click context object
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Typed CliContext object
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
click.ClickException: If context object is not properly initialized
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> @buckets.command()
|
|
52
|
+
>>> @click.pass_context
|
|
53
|
+
>>> def list(ctx):
|
|
54
|
+
... cli_ctx = get_cli_context(ctx) # Fully typed!
|
|
55
|
+
... print(cli_ctx.output_format) # Autocomplete works
|
|
56
|
+
"""
|
|
57
|
+
if not isinstance(ctx.obj, CliContext):
|
|
58
|
+
raise click.ClickException(
|
|
59
|
+
"Internal error: CLI context not initialized. "
|
|
60
|
+
"This is a bug - please report it."
|
|
61
|
+
)
|
|
62
|
+
return ctx.obj
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
pass_cli_context = click.make_pass_decorator(CliContext, ensure=True)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Output formatting for different output modes.
|
|
2
|
+
|
|
3
|
+
This module provides consistent output formatting across all CLI commands,
|
|
4
|
+
supporting multiple formats (table, JSON, CSV) with proper handling of
|
|
5
|
+
Pydantic models, iterators, and large datasets.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import csv
|
|
9
|
+
import json
|
|
10
|
+
from collections.abc import Generator, Iterator
|
|
11
|
+
from io import StringIO
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_output(
|
|
19
|
+
data: Any,
|
|
20
|
+
fmt: str = "table",
|
|
21
|
+
output: Optional[str] = None,
|
|
22
|
+
no_color: bool = False,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Format and output data to stdout or file.
|
|
25
|
+
|
|
26
|
+
Handles:
|
|
27
|
+
- Pydantic models (via model_dump())
|
|
28
|
+
- Iterators and generators (converts to list)
|
|
29
|
+
- Lists, dicts, primitives
|
|
30
|
+
- Large datasets (warns at 10k+ items)
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
data: Data to format
|
|
34
|
+
fmt: Output format (json, table, csv)
|
|
35
|
+
output: Optional file path to write to
|
|
36
|
+
no_color: Disable colored output for table format
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> format_output([{"name": "bucket1"}, {"name": "bucket2"}], fmt="table")
|
|
40
|
+
name
|
|
41
|
+
--------
|
|
42
|
+
bucket1
|
|
43
|
+
bucket2
|
|
44
|
+
"""
|
|
45
|
+
if isinstance(data, (Iterator, Generator)):
|
|
46
|
+
data = list(data)
|
|
47
|
+
# Warn about large datasets
|
|
48
|
+
if len(data) > 10000:
|
|
49
|
+
click.echo(
|
|
50
|
+
f"Warning: Loading {len(data)} items into memory for formatting. "
|
|
51
|
+
"This may be slow or cause memory issues. "
|
|
52
|
+
"Consider using --limit or redirecting output for large datasets.",
|
|
53
|
+
err=True,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if hasattr(data, "model_dump"):
|
|
57
|
+
data = data.model_dump()
|
|
58
|
+
elif isinstance(data, list) and len(data) > 0 and hasattr(data[0], "model_dump"):
|
|
59
|
+
data = [item.model_dump() for item in data]
|
|
60
|
+
|
|
61
|
+
if hasattr(data, "__aiter__"):
|
|
62
|
+
raise TypeError(
|
|
63
|
+
"Async iterators not supported in CLI output. "
|
|
64
|
+
"Use synchronous methods or convert to list first."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if output and fmt == "table":
|
|
68
|
+
no_color = True
|
|
69
|
+
|
|
70
|
+
if fmt == "json":
|
|
71
|
+
text = _format_json(data)
|
|
72
|
+
elif fmt == "table":
|
|
73
|
+
text = _format_table(data, no_color=no_color)
|
|
74
|
+
elif fmt == "csv":
|
|
75
|
+
text = _format_csv(data)
|
|
76
|
+
else:
|
|
77
|
+
text = str(data)
|
|
78
|
+
|
|
79
|
+
if output:
|
|
80
|
+
Path(output).write_text(text, encoding="utf-8")
|
|
81
|
+
click.echo(f"Output written to {output}", err=True)
|
|
82
|
+
else:
|
|
83
|
+
click.echo(text)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _format_json(data: Any) -> str:
|
|
87
|
+
"""Format data as JSON.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
data: Data to format
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
JSON string with 2-space indentation
|
|
94
|
+
"""
|
|
95
|
+
return json.dumps(data, indent=2, default=str)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _format_table(data: Any, no_color: bool = False) -> str:
|
|
99
|
+
"""Format data as simple table (AWS/Azure CLI style).
|
|
100
|
+
|
|
101
|
+
Uses a simple ASCII table format with column alignment similar to
|
|
102
|
+
AWS CLI and Azure CLI, avoiding fancy box-drawing characters.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
data: Data to format
|
|
106
|
+
no_color: Disable colored output (ignored, kept for API compatibility)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Formatted table string
|
|
110
|
+
"""
|
|
111
|
+
items = data if isinstance(data, list) else [data]
|
|
112
|
+
if not items:
|
|
113
|
+
return "No results"
|
|
114
|
+
|
|
115
|
+
if not isinstance(items[0], dict):
|
|
116
|
+
items = [{"value": str(item)} for item in items]
|
|
117
|
+
|
|
118
|
+
columns = list(items[0].keys())
|
|
119
|
+
|
|
120
|
+
str_items = []
|
|
121
|
+
for item in items:
|
|
122
|
+
str_items.append({col: str(item.get(col, "")) for col in columns})
|
|
123
|
+
|
|
124
|
+
col_widths = {}
|
|
125
|
+
for col in columns:
|
|
126
|
+
col_widths[col] = len(col)
|
|
127
|
+
for item in str_items:
|
|
128
|
+
col_widths[col] = max(col_widths[col], len(item[col]))
|
|
129
|
+
|
|
130
|
+
header_parts = []
|
|
131
|
+
separator_parts = []
|
|
132
|
+
for col in columns:
|
|
133
|
+
header_parts.append(col.ljust(col_widths[col]))
|
|
134
|
+
separator_parts.append("-" * col_widths[col])
|
|
135
|
+
|
|
136
|
+
header = " ".join(header_parts)
|
|
137
|
+
separator = " ".join(separator_parts)
|
|
138
|
+
|
|
139
|
+
rows = []
|
|
140
|
+
for item in str_items:
|
|
141
|
+
row_parts = [item[col].ljust(col_widths[col]) for col in columns]
|
|
142
|
+
rows.append(" ".join(row_parts))
|
|
143
|
+
|
|
144
|
+
return "\n".join([header, separator, *rows])
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _format_csv(data: Any) -> str:
|
|
148
|
+
"""Format data as CSV.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
data: Data to format
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
CSV string with header
|
|
155
|
+
"""
|
|
156
|
+
items = data if isinstance(data, list) else [data]
|
|
157
|
+
if not items:
|
|
158
|
+
return ""
|
|
159
|
+
|
|
160
|
+
if not isinstance(items[0], dict):
|
|
161
|
+
items = [{"value": str(item)} for item in items]
|
|
162
|
+
|
|
163
|
+
columns = list(items[0].keys())
|
|
164
|
+
|
|
165
|
+
output = StringIO()
|
|
166
|
+
writer = csv.DictWriter(output, fieldnames=columns, extrasaction="ignore")
|
|
167
|
+
writer.writeheader()
|
|
168
|
+
|
|
169
|
+
for item in items:
|
|
170
|
+
normalized_row = {col: item.get(col, "") for col in columns}
|
|
171
|
+
writer.writerow(normalized_row)
|
|
172
|
+
|
|
173
|
+
return output.getvalue()
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Base utilities for service commands.
|
|
2
|
+
|
|
3
|
+
This module provides decorators and utilities for implementing service-specific
|
|
4
|
+
CLI commands with consistent error handling, async support, and output formatting.
|
|
5
|
+
|
|
6
|
+
Key features:
|
|
7
|
+
- Sequential decorator composition
|
|
8
|
+
- Type-safe context access
|
|
9
|
+
- Enhanced Click exception handling
|
|
10
|
+
- Async/await support
|
|
11
|
+
- Consistent error messages
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import inspect
|
|
16
|
+
from functools import wraps
|
|
17
|
+
from typing import Any, Callable
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
from httpx import HTTPError
|
|
21
|
+
|
|
22
|
+
from ...models.errors import BaseUrlMissingError, SecretMissingError
|
|
23
|
+
from ...models.exceptions import EnrichedException
|
|
24
|
+
from ._context import get_cli_context
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def handle_not_found_error(
|
|
28
|
+
resource: str, identifier: str, error: Exception | None = None
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Handle 404/LookupError and raise consistent ClickException.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
resource: The resource type (e.g., "Bucket", "Asset", "Queue")
|
|
34
|
+
identifier: The resource identifier (name, key, etc.)
|
|
35
|
+
error: Optional original error for chaining
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
click.ClickException: Always raises with consistent message
|
|
39
|
+
"""
|
|
40
|
+
message = f"{resource} '{identifier}' not found."
|
|
41
|
+
if error:
|
|
42
|
+
raise click.ClickException(message) from error
|
|
43
|
+
else:
|
|
44
|
+
raise click.ClickException(message) from None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def service_command(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
48
|
+
"""Decorator for service commands with async support, error handling, and output.
|
|
49
|
+
|
|
50
|
+
This decorator handles:
|
|
51
|
+
- Sync and async function execution
|
|
52
|
+
- Output formatting (JSON, table, CSV)
|
|
53
|
+
- Error handling with proper Click exceptions
|
|
54
|
+
- Separation of logs (stderr) from data (stdout)
|
|
55
|
+
- Type-safe context access via get_cli_context()
|
|
56
|
+
- Enhanced Click exception handling (don't catch Click's own exceptions)
|
|
57
|
+
- Domain errors converted to click.ClickException
|
|
58
|
+
- Automatic context injection (no need for @click.pass_context)
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
>>> @buckets.command()
|
|
62
|
+
>>> @service_command
|
|
63
|
+
>>> async def list_async(ctx):
|
|
64
|
+
... # Context is automatically passed - no @click.pass_context needed
|
|
65
|
+
... return await client.buckets.list_async()
|
|
66
|
+
|
|
67
|
+
Note:
|
|
68
|
+
Do NOT stack @click.pass_context with this decorator - context is
|
|
69
|
+
already injected by the @wraps(f) and @click.pass_context inside
|
|
70
|
+
service_command.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
@wraps(f)
|
|
74
|
+
@click.pass_context
|
|
75
|
+
def wrapper(ctx, *args, **kwargs):
|
|
76
|
+
cli_ctx = get_cli_context(ctx)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
result = f(ctx, *args, **kwargs)
|
|
80
|
+
|
|
81
|
+
if inspect.isawaitable(result):
|
|
82
|
+
try:
|
|
83
|
+
result = asyncio.run(result) # type: ignore[arg-type]
|
|
84
|
+
except RuntimeError as e:
|
|
85
|
+
if "cannot be called from a running event loop" in str(e).lower():
|
|
86
|
+
prev_loop = asyncio.get_event_loop()
|
|
87
|
+
if prev_loop.is_running():
|
|
88
|
+
loop = asyncio.new_event_loop()
|
|
89
|
+
try:
|
|
90
|
+
asyncio.set_event_loop(loop)
|
|
91
|
+
result = loop.run_until_complete(result)
|
|
92
|
+
finally:
|
|
93
|
+
try:
|
|
94
|
+
loop.close()
|
|
95
|
+
finally:
|
|
96
|
+
asyncio.set_event_loop(prev_loop)
|
|
97
|
+
else:
|
|
98
|
+
result = prev_loop.run_until_complete(result)
|
|
99
|
+
else:
|
|
100
|
+
raise
|
|
101
|
+
|
|
102
|
+
# Format and output result
|
|
103
|
+
if result is not None:
|
|
104
|
+
from ._formatters import format_output
|
|
105
|
+
|
|
106
|
+
fmt = kwargs.get("format") or cli_ctx.output_format
|
|
107
|
+
output = kwargs.get("output")
|
|
108
|
+
|
|
109
|
+
format_output(
|
|
110
|
+
result,
|
|
111
|
+
fmt=fmt,
|
|
112
|
+
output=output,
|
|
113
|
+
no_color=False, # Auto-detected for file output
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
except click.ClickException:
|
|
119
|
+
raise
|
|
120
|
+
|
|
121
|
+
except BaseUrlMissingError:
|
|
122
|
+
raise click.ClickException(
|
|
123
|
+
"UIPATH_URL not configured. Set the UIPATH_URL environment variable or run 'uipath auth'."
|
|
124
|
+
) from None
|
|
125
|
+
|
|
126
|
+
except SecretMissingError:
|
|
127
|
+
raise click.ClickException(
|
|
128
|
+
"Authentication required. Set the UIPATH_ACCESS_TOKEN environment variable or run 'uipath auth'."
|
|
129
|
+
) from None
|
|
130
|
+
|
|
131
|
+
except EnrichedException as e:
|
|
132
|
+
if cli_ctx.debug:
|
|
133
|
+
raise
|
|
134
|
+
|
|
135
|
+
if e.status_code == 401:
|
|
136
|
+
raise click.ClickException(
|
|
137
|
+
"Authentication failed (401). Your access token may have expired or is invalid.\n"
|
|
138
|
+
"Please run 'uipath auth' to re-authenticate."
|
|
139
|
+
) from e
|
|
140
|
+
|
|
141
|
+
if e.status_code == 400:
|
|
142
|
+
try:
|
|
143
|
+
import json
|
|
144
|
+
|
|
145
|
+
error_data = json.loads(e.response_content)
|
|
146
|
+
if (
|
|
147
|
+
isinstance(error_data, dict)
|
|
148
|
+
and error_data.get("errorCode") == 1101
|
|
149
|
+
):
|
|
150
|
+
raise click.ClickException(
|
|
151
|
+
"Folder context required (400). The command requires a folder to be specified.\n"
|
|
152
|
+
'Set UIPATH_FOLDER_PATH environment variable (e.g., export UIPATH_FOLDER_PATH="Shared") '
|
|
153
|
+
'or use the --folder-path option (e.g., --folder-path "Shared").'
|
|
154
|
+
) from e
|
|
155
|
+
except (ValueError, json.JSONDecodeError):
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
raise click.ClickException(str(e)) from e
|
|
159
|
+
|
|
160
|
+
except HTTPError as e:
|
|
161
|
+
if cli_ctx.debug:
|
|
162
|
+
raise
|
|
163
|
+
response = getattr(e, "response", None)
|
|
164
|
+
|
|
165
|
+
if response and response.status_code == 401:
|
|
166
|
+
raise click.ClickException(
|
|
167
|
+
"Authentication failed (401). Your access token may have expired or is invalid.\n"
|
|
168
|
+
"Please run 'uipath auth' to re-authenticate."
|
|
169
|
+
) from e
|
|
170
|
+
|
|
171
|
+
if response and response.status_code == 400:
|
|
172
|
+
try:
|
|
173
|
+
error_data = response.json()
|
|
174
|
+
if (
|
|
175
|
+
isinstance(error_data, dict)
|
|
176
|
+
and error_data.get("errorCode") == 1101
|
|
177
|
+
):
|
|
178
|
+
raise click.ClickException(
|
|
179
|
+
"Folder context required (400). The command requires a folder to be specified.\n"
|
|
180
|
+
'Set UIPATH_FOLDER_PATH environment variable (e.g., export UIPATH_FOLDER_PATH="Shared") '
|
|
181
|
+
'or use the --folder-path option (e.g., --folder-path "Shared").'
|
|
182
|
+
) from e
|
|
183
|
+
except ValueError:
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
if response:
|
|
187
|
+
error_msg = f"HTTP Error {response.status_code}: {response.url}"
|
|
188
|
+
if hasattr(response, "text"):
|
|
189
|
+
try:
|
|
190
|
+
import json
|
|
191
|
+
|
|
192
|
+
response_data = response.json()
|
|
193
|
+
sensitive_fields = {
|
|
194
|
+
"access_token",
|
|
195
|
+
"refresh_token",
|
|
196
|
+
"password",
|
|
197
|
+
"secret",
|
|
198
|
+
"api_key",
|
|
199
|
+
"authorization",
|
|
200
|
+
}
|
|
201
|
+
if isinstance(response_data, dict):
|
|
202
|
+
for key in list(response_data.keys()):
|
|
203
|
+
if any(
|
|
204
|
+
sensitive in key.lower()
|
|
205
|
+
for sensitive in sensitive_fields
|
|
206
|
+
):
|
|
207
|
+
response_data[key] = "***REDACTED***"
|
|
208
|
+
error_details = json.dumps(response_data, indent=2)
|
|
209
|
+
error_msg += f"\nResponse:\n{error_details[:500]}"
|
|
210
|
+
except Exception:
|
|
211
|
+
error_msg += f"\nResponse: {response.text[:200]}"
|
|
212
|
+
else:
|
|
213
|
+
error_msg = f"HTTP Error: {str(e)}"
|
|
214
|
+
raise click.ClickException(error_msg) from e
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
if cli_ctx.debug:
|
|
218
|
+
raise
|
|
219
|
+
raise click.ClickException(str(e)) from e
|
|
220
|
+
|
|
221
|
+
return wrapper
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def common_service_options(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
225
|
+
"""Add common options for service commands.
|
|
226
|
+
|
|
227
|
+
Adds:
|
|
228
|
+
- --folder-path: Folder path (e.g., "Shared") with validation
|
|
229
|
+
- --folder-key: Folder key (UUID) with validation
|
|
230
|
+
- --format: Output format override
|
|
231
|
+
- --output: Output file override
|
|
232
|
+
"""
|
|
233
|
+
from ._validators import validate_folder_path, validate_uuid
|
|
234
|
+
|
|
235
|
+
decorators = [
|
|
236
|
+
click.option(
|
|
237
|
+
"--folder-path",
|
|
238
|
+
callback=validate_folder_path,
|
|
239
|
+
help='Folder path (e.g., "Shared"). Can also be set via UIPATH_FOLDER_PATH environment variable.',
|
|
240
|
+
),
|
|
241
|
+
click.option("--folder-key", callback=validate_uuid, help="Folder key (UUID)"),
|
|
242
|
+
click.option(
|
|
243
|
+
"--format",
|
|
244
|
+
type=click.Choice(["json", "table", "csv"]),
|
|
245
|
+
help="Output format (overrides global)",
|
|
246
|
+
),
|
|
247
|
+
click.option(
|
|
248
|
+
"--output", "-o", type=click.Path(), help="Output file (overrides global)"
|
|
249
|
+
),
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
for decorator in reversed(decorators):
|
|
253
|
+
f = decorator(f)
|
|
254
|
+
|
|
255
|
+
return f
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def standard_service_command(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
259
|
+
"""Convenience decorator for standard service commands.
|
|
260
|
+
|
|
261
|
+
This composes decorators in explicit, sequential order:
|
|
262
|
+
1. service_command (execution & error handling)
|
|
263
|
+
2. common_service_options (folder, format, output)
|
|
264
|
+
|
|
265
|
+
The sequential pattern makes the decorator stack more readable
|
|
266
|
+
and easier for AI agents to understand.
|
|
267
|
+
|
|
268
|
+
Usage:
|
|
269
|
+
>>> # Simple command
|
|
270
|
+
>>> @buckets.command()
|
|
271
|
+
>>> @standard_service_command
|
|
272
|
+
>>> def retrieve(ctx, name, folder_path, ...):
|
|
273
|
+
... pass
|
|
274
|
+
|
|
275
|
+
>>> # Custom composition (when you need flexibility)
|
|
276
|
+
>>> @buckets.command()
|
|
277
|
+
>>> @service_command
|
|
278
|
+
>>> @click.option('--custom-flag', ...)
|
|
279
|
+
>>> @common_service_options
|
|
280
|
+
>>> @click.pass_context
|
|
281
|
+
>>> def special(ctx, custom_flag, ...):
|
|
282
|
+
... pass
|
|
283
|
+
"""
|
|
284
|
+
decorated_func = f
|
|
285
|
+
|
|
286
|
+
decorated_func = service_command(decorated_func)
|
|
287
|
+
|
|
288
|
+
decorated_func = common_service_options(decorated_func)
|
|
289
|
+
|
|
290
|
+
return decorated_func
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class ServiceCommandBase:
|
|
294
|
+
"""Base class for service command utilities."""
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def get_client(ctx):
|
|
298
|
+
"""Get or create UiPath client from context.
|
|
299
|
+
|
|
300
|
+
This caches the client in the CLI context to avoid recreating
|
|
301
|
+
it for every command invocation.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
ctx: Click context
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
UiPath client instance
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
click.ClickException: If required environment variables are not set
|
|
311
|
+
"""
|
|
312
|
+
import os
|
|
313
|
+
|
|
314
|
+
import click
|
|
315
|
+
|
|
316
|
+
cli_ctx = get_cli_context(ctx)
|
|
317
|
+
|
|
318
|
+
if cli_ctx._client is None:
|
|
319
|
+
from ..._uipath import UiPath
|
|
320
|
+
|
|
321
|
+
base_url = os.environ.get("UIPATH_URL")
|
|
322
|
+
secret = os.environ.get("UIPATH_ACCESS_TOKEN")
|
|
323
|
+
|
|
324
|
+
if not base_url:
|
|
325
|
+
raise click.ClickException(
|
|
326
|
+
"UIPATH_URL not configured. Set the UIPATH_URL environment variable or run 'uipath auth'."
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if not secret:
|
|
330
|
+
raise click.ClickException(
|
|
331
|
+
"Authentication required. Set the UIPATH_ACCESS_TOKEN environment variable or run 'uipath auth'."
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
cli_ctx._client = UiPath(
|
|
335
|
+
base_url=base_url,
|
|
336
|
+
secret=secret,
|
|
337
|
+
debug=cli_ctx.debug,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
return cli_ctx._client
|