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 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
- def cli(lv: bool, v: bool) -> None:
50
- load_environment_variables()
51
- add_cwd_to_path()
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.logs_interceptor = LogsInterceptor(
635
- min_level=self.context.logs_min_level,
636
- dir=self.context.runtime_dir,
637
- file=self.context.logs_file,
638
- job_id=self.context.job_id,
639
- execution_id=self.context.execution_id,
640
- is_debug_run=self.is_debug_run(),
641
- log_handler=self.context.log_handler,
642
- )
643
- self.logs_interceptor.setup()
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