uipath 2.1.131__py3-none-any.whl → 2.1.132__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-2.1.131.dist-info → uipath-2.1.132.dist-info}/METADATA +1 -1
- {uipath-2.1.131.dist-info → uipath-2.1.132.dist-info}/RECORD +21 -10
- {uipath-2.1.131.dist-info → uipath-2.1.132.dist-info}/WHEEL +0 -0
- {uipath-2.1.131.dist-info → uipath-2.1.132.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.131.dist-info → uipath-2.1.132.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
"""Service CLI Generator for automatic CRUD command generation.
|
|
2
|
+
|
|
3
|
+
This module provides ServiceCLIGenerator, which automatically generates
|
|
4
|
+
standard Click CLI commands for services implementing CRUDServiceProtocol.
|
|
5
|
+
|
|
6
|
+
The generator creates commands for:
|
|
7
|
+
- list: List all resources
|
|
8
|
+
- retrieve: Get a single resource
|
|
9
|
+
- create: Create a new resource
|
|
10
|
+
- delete: Delete a resource
|
|
11
|
+
- exists: Check if a resource exists (if service supports it)
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> from ._service_metadata import ServiceMetadata, CreateParameter
|
|
15
|
+
>>> from ._service_cli_generator import ServiceCLIGenerator
|
|
16
|
+
>>>
|
|
17
|
+
>>> metadata = ServiceMetadata(
|
|
18
|
+
... service_name="buckets",
|
|
19
|
+
... service_attr="buckets",
|
|
20
|
+
... resource_type="Bucket",
|
|
21
|
+
... create_params={
|
|
22
|
+
... "description": CreateParameter(
|
|
23
|
+
... type="str",
|
|
24
|
+
... required=False,
|
|
25
|
+
... help="Bucket description",
|
|
26
|
+
... )
|
|
27
|
+
... },
|
|
28
|
+
... )
|
|
29
|
+
>>>
|
|
30
|
+
>>> generator = ServiceCLIGenerator(metadata)
|
|
31
|
+
>>> buckets_group = generator.register(parent_group)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from enum import Enum
|
|
35
|
+
from itertools import islice
|
|
36
|
+
from typing import Any, Callable, TypedDict
|
|
37
|
+
|
|
38
|
+
import click
|
|
39
|
+
|
|
40
|
+
from ._service_base import (
|
|
41
|
+
ServiceCommandBase,
|
|
42
|
+
common_service_options,
|
|
43
|
+
service_command,
|
|
44
|
+
)
|
|
45
|
+
from ._service_metadata import ServiceMetadata
|
|
46
|
+
from ._service_protocol import has_exists_method, validate_service_protocol
|
|
47
|
+
from ._type_registry import get_type
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GeneratorType(str, Enum):
|
|
51
|
+
"""Enum for command generator types.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
AUTO: Auto-generated by ServiceCLIGenerator
|
|
55
|
+
MANUAL: Manually added nested group or custom command
|
|
56
|
+
OVERRIDE: Manual override of auto-generated command
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
AUTO = "ServiceCLIGenerator"
|
|
60
|
+
MANUAL = "manual"
|
|
61
|
+
OVERRIDE = "manual_override"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _optional_decorator(
|
|
65
|
+
condition: bool, decorator: Callable[[Any], Any]
|
|
66
|
+
) -> Callable[[Any], Any]:
|
|
67
|
+
"""Apply decorator conditionally based on a boolean flag.
|
|
68
|
+
|
|
69
|
+
This makes conditional decorator patterns more explicit and readable.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
condition: Whether to apply the decorator
|
|
73
|
+
decorator: The decorator function to apply if condition is True
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The decorator if condition is True, otherwise an identity function
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> @_optional_decorator(
|
|
80
|
+
... condition=config.confirmation_required,
|
|
81
|
+
... decorator=click.option("--confirm", is_flag=True)
|
|
82
|
+
... )
|
|
83
|
+
... def my_command():
|
|
84
|
+
... pass
|
|
85
|
+
"""
|
|
86
|
+
if condition:
|
|
87
|
+
return decorator
|
|
88
|
+
return lambda f: f
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ProvenanceDict(TypedDict, total=False):
|
|
92
|
+
"""Type definition for command provenance tracking.
|
|
93
|
+
|
|
94
|
+
Attributes:
|
|
95
|
+
generator: Source of the command (ServiceCLIGenerator, manual, manual_override)
|
|
96
|
+
service_name: Name of the service
|
|
97
|
+
resource_type: Type of resource (e.g., "Bucket")
|
|
98
|
+
command_function: Name of the command function
|
|
99
|
+
type: Type of manual addition (e.g., "nested_group")
|
|
100
|
+
original_generator: Original generator for overridden commands
|
|
101
|
+
overridden_at: Context where override occurred (e.g., "user_custom")
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
generator: str
|
|
105
|
+
service_name: str
|
|
106
|
+
resource_type: str
|
|
107
|
+
command_function: str
|
|
108
|
+
type: str
|
|
109
|
+
original_generator: str
|
|
110
|
+
overridden_at: str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ServiceCLIGenerator:
|
|
114
|
+
"""Generates CLI commands for CRUD services.
|
|
115
|
+
|
|
116
|
+
This class automatically creates Click commands for services that implement
|
|
117
|
+
the CRUDServiceProtocol. It handles:
|
|
118
|
+
- Command generation from metadata
|
|
119
|
+
- Parameter type resolution
|
|
120
|
+
- Confirmation and dry-run support
|
|
121
|
+
- Output formatting integration
|
|
122
|
+
- Provenance tracking
|
|
123
|
+
|
|
124
|
+
Attributes:
|
|
125
|
+
metadata: Service metadata configuration
|
|
126
|
+
_command_names: Set of generated command names
|
|
127
|
+
_provenance: Dict tracking command generation metadata
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> metadata = ServiceMetadata(
|
|
131
|
+
... service_name="buckets",
|
|
132
|
+
... service_attr="buckets",
|
|
133
|
+
... resource_type="Bucket",
|
|
134
|
+
... )
|
|
135
|
+
>>> generator = ServiceCLIGenerator(metadata)
|
|
136
|
+
>>> buckets_group = generator.register(parent_group)
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self, metadata: ServiceMetadata) -> None:
|
|
140
|
+
"""Initialize the generator with service metadata.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
metadata: Service metadata configuration
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ValueError: If metadata has invalid types
|
|
147
|
+
"""
|
|
148
|
+
self.metadata = metadata
|
|
149
|
+
self._command_names: set[str] = set()
|
|
150
|
+
self._provenance: dict[str, ProvenanceDict] = {}
|
|
151
|
+
|
|
152
|
+
self.metadata.validate_types()
|
|
153
|
+
|
|
154
|
+
def validate_service(self, client: Any) -> None:
|
|
155
|
+
"""Validate that the service implements the required protocol.
|
|
156
|
+
|
|
157
|
+
This method should be called early during CLI setup to fail fast
|
|
158
|
+
if the service doesn't implement the required CRUD methods.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
client: Client instance with service attributes
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
TypeError: If service doesn't implement CRUDServiceProtocol
|
|
165
|
+
AttributeError: If service attribute doesn't exist on client
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
>>> generator = ServiceCLIGenerator(metadata)
|
|
169
|
+
>>> from uipath import UiPath
|
|
170
|
+
>>> client = UiPath(...)
|
|
171
|
+
>>> generator.validate_service(client) # Fails early if invalid
|
|
172
|
+
>>> generator.register(cli_group) # Now safe to register
|
|
173
|
+
"""
|
|
174
|
+
service = getattr(client, self.metadata.service_attr)
|
|
175
|
+
validate_service_protocol(service, self.metadata.service_name)
|
|
176
|
+
|
|
177
|
+
def register(self, parent_group: click.Group) -> click.Group:
|
|
178
|
+
"""Register service commands under a parent Click group.
|
|
179
|
+
|
|
180
|
+
This creates a new Click group for the service and adds all
|
|
181
|
+
CRUD commands to it based on the metadata.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
parent_group: Parent Click group to add service group to
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
The created service Click group
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
TypeError: If service doesn't implement CRUDServiceProtocol
|
|
191
|
+
|
|
192
|
+
Example:
|
|
193
|
+
>>> @click.group()
|
|
194
|
+
... def cli():
|
|
195
|
+
... pass
|
|
196
|
+
>>> generator = ServiceCLIGenerator(metadata)
|
|
197
|
+
>>> service_group = generator.register(cli)
|
|
198
|
+
"""
|
|
199
|
+
service_group = click.Group(
|
|
200
|
+
name=self.metadata.service_name,
|
|
201
|
+
help=f"Manage {self.metadata.resource_plural}",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
self._add_list_command(service_group)
|
|
205
|
+
self._add_retrieve_command(service_group)
|
|
206
|
+
self._add_create_command(service_group)
|
|
207
|
+
self._add_delete_command(service_group)
|
|
208
|
+
|
|
209
|
+
self._add_exists_command(service_group)
|
|
210
|
+
|
|
211
|
+
parent_group.add_command(service_group)
|
|
212
|
+
|
|
213
|
+
return service_group
|
|
214
|
+
|
|
215
|
+
def _add_list_command(self, group: click.Group) -> None:
|
|
216
|
+
"""Add list command to the group.
|
|
217
|
+
|
|
218
|
+
Generates: uipath <service> list [--folder-path PATH] [--folder-key KEY] [--limit N] [--offset N]
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
@group.command("list")
|
|
222
|
+
@click.option("--limit", type=int, help="Maximum number of items to return")
|
|
223
|
+
@click.option("--offset", type=int, default=0, help="Number of items to skip")
|
|
224
|
+
@common_service_options
|
|
225
|
+
@service_command
|
|
226
|
+
def list_cmd(
|
|
227
|
+
ctx: click.Context,
|
|
228
|
+
folder_path: str | None,
|
|
229
|
+
folder_key: str | None,
|
|
230
|
+
format: str | None,
|
|
231
|
+
output: str | None,
|
|
232
|
+
limit: int | None,
|
|
233
|
+
offset: int,
|
|
234
|
+
) -> list[Any]:
|
|
235
|
+
"""List resources."""
|
|
236
|
+
client = ServiceCommandBase.get_client(ctx)
|
|
237
|
+
service = getattr(client, self.metadata.service_attr)
|
|
238
|
+
|
|
239
|
+
resources = service.list(folder_path=folder_path, folder_key=folder_key)
|
|
240
|
+
|
|
241
|
+
if limit is not None:
|
|
242
|
+
return list(islice(resources, offset, offset + limit))
|
|
243
|
+
elif offset > 0:
|
|
244
|
+
return list(islice(resources, offset, None))
|
|
245
|
+
else:
|
|
246
|
+
return list(resources)
|
|
247
|
+
|
|
248
|
+
list_cmd.help = f"""List all {self.metadata.resource_plural}.
|
|
249
|
+
|
|
250
|
+
Examples:
|
|
251
|
+
uipath {self.metadata.service_name} list
|
|
252
|
+
uipath {self.metadata.service_name} list --folder-path Shared
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
self._track_command("list", list_cmd)
|
|
256
|
+
|
|
257
|
+
def _add_retrieve_command(self, group: click.Group) -> None:
|
|
258
|
+
"""Add retrieve command to the group.
|
|
259
|
+
|
|
260
|
+
Generates: uipath <service> retrieve <name> [--folder-path PATH]
|
|
261
|
+
"""
|
|
262
|
+
identifier = self.metadata.retrieve_identifier
|
|
263
|
+
|
|
264
|
+
@group.command("retrieve")
|
|
265
|
+
@click.argument(identifier.upper())
|
|
266
|
+
@common_service_options
|
|
267
|
+
@service_command
|
|
268
|
+
def retrieve_cmd(
|
|
269
|
+
ctx: click.Context,
|
|
270
|
+
folder_path: str | None,
|
|
271
|
+
folder_key: str | None,
|
|
272
|
+
format: str | None,
|
|
273
|
+
output: str | None,
|
|
274
|
+
**kwargs: Any,
|
|
275
|
+
) -> None:
|
|
276
|
+
"""Retrieve a resource."""
|
|
277
|
+
client = ServiceCommandBase.get_client(ctx)
|
|
278
|
+
service = getattr(client, self.metadata.service_attr)
|
|
279
|
+
|
|
280
|
+
identifier_value = kwargs.get(identifier)
|
|
281
|
+
|
|
282
|
+
resource = service.retrieve(
|
|
283
|
+
identifier_value, folder_path=folder_path, folder_key=folder_key
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return resource
|
|
287
|
+
|
|
288
|
+
retrieve_cmd.help = f"""Retrieve a single {self.metadata.resource_type}.
|
|
289
|
+
|
|
290
|
+
Examples:
|
|
291
|
+
uipath {self.metadata.service_name} retrieve my-resource
|
|
292
|
+
uipath {self.metadata.service_name} retrieve my-resource --folder-path Shared
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
self._track_command("retrieve", retrieve_cmd)
|
|
296
|
+
|
|
297
|
+
def _add_create_command(self, group: click.Group) -> None:
|
|
298
|
+
"""Add create command to the group.
|
|
299
|
+
|
|
300
|
+
Generates: uipath <service> create <name> [options] [--folder-path PATH]
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
def create_command_decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
304
|
+
"""Apply all create parameter decorators to the function."""
|
|
305
|
+
for param_name, param_config in reversed(
|
|
306
|
+
list(self.metadata.create_params.items())
|
|
307
|
+
):
|
|
308
|
+
option_name = param_config.option_name or param_name
|
|
309
|
+
param_type = get_type(param_config.type)
|
|
310
|
+
|
|
311
|
+
# Build Click option
|
|
312
|
+
func = click.option(
|
|
313
|
+
f"--{option_name.replace('_', '-')}",
|
|
314
|
+
type=param_type,
|
|
315
|
+
required=param_config.required,
|
|
316
|
+
default=param_config.default,
|
|
317
|
+
help=param_config.help,
|
|
318
|
+
)(func)
|
|
319
|
+
|
|
320
|
+
return func
|
|
321
|
+
|
|
322
|
+
@group.command("create")
|
|
323
|
+
@click.argument("NAME")
|
|
324
|
+
@create_command_decorator
|
|
325
|
+
@common_service_options
|
|
326
|
+
@service_command
|
|
327
|
+
def create_cmd(
|
|
328
|
+
ctx: click.Context,
|
|
329
|
+
folder_path: str | None,
|
|
330
|
+
folder_key: str | None,
|
|
331
|
+
format: str | None,
|
|
332
|
+
output: str | None,
|
|
333
|
+
name: str,
|
|
334
|
+
**kwargs: Any,
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Create a resource."""
|
|
337
|
+
client = ServiceCommandBase.get_client(ctx)
|
|
338
|
+
service = getattr(client, self.metadata.service_attr)
|
|
339
|
+
|
|
340
|
+
create_kwargs = {}
|
|
341
|
+
for param_name in self.metadata.create_params.keys():
|
|
342
|
+
if param_name in kwargs and kwargs[param_name] is not None:
|
|
343
|
+
create_kwargs[param_name] = kwargs[param_name]
|
|
344
|
+
|
|
345
|
+
resource = service.create(
|
|
346
|
+
name, folder_path=folder_path, folder_key=folder_key, **create_kwargs
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
click.echo(
|
|
350
|
+
f"Created {self.metadata.resource_type.lower()} '{name}'", err=True
|
|
351
|
+
)
|
|
352
|
+
return resource
|
|
353
|
+
|
|
354
|
+
create_cmd.help = f"""Create a new {self.metadata.resource_type}.
|
|
355
|
+
|
|
356
|
+
Examples:
|
|
357
|
+
uipath {self.metadata.service_name} create my-resource
|
|
358
|
+
uipath {self.metadata.service_name} create my-resource --folder-path Shared
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
self._track_command("create", create_cmd)
|
|
362
|
+
|
|
363
|
+
def _add_delete_command(self, group: click.Group) -> None:
|
|
364
|
+
"""Add delete command to the group.
|
|
365
|
+
|
|
366
|
+
Generates: uipath <service> delete <name> [--confirm] [--dry-run] [--folder-path PATH]
|
|
367
|
+
"""
|
|
368
|
+
delete_config = self.metadata.delete_cmd
|
|
369
|
+
|
|
370
|
+
@group.command("delete")
|
|
371
|
+
@click.argument("NAME")
|
|
372
|
+
@_optional_decorator(
|
|
373
|
+
delete_config.confirmation_required,
|
|
374
|
+
click.option(
|
|
375
|
+
"--confirm",
|
|
376
|
+
is_flag=True,
|
|
377
|
+
help="Confirm deletion without prompting",
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
@_optional_decorator(
|
|
381
|
+
delete_config.dry_run_supported,
|
|
382
|
+
click.option(
|
|
383
|
+
"--dry-run",
|
|
384
|
+
is_flag=True,
|
|
385
|
+
help="Show what would be deleted without actually deleting",
|
|
386
|
+
),
|
|
387
|
+
)
|
|
388
|
+
@common_service_options
|
|
389
|
+
@service_command
|
|
390
|
+
def delete_cmd(
|
|
391
|
+
ctx: click.Context,
|
|
392
|
+
folder_path: str | None,
|
|
393
|
+
folder_key: str | None,
|
|
394
|
+
format: str | None,
|
|
395
|
+
output: str | None,
|
|
396
|
+
name: str,
|
|
397
|
+
**kwargs: Any,
|
|
398
|
+
) -> None:
|
|
399
|
+
"""Delete a resource."""
|
|
400
|
+
client = ServiceCommandBase.get_client(ctx)
|
|
401
|
+
service = getattr(client, self.metadata.service_attr)
|
|
402
|
+
|
|
403
|
+
if delete_config.dry_run_supported:
|
|
404
|
+
dry_run = kwargs.get("dry_run", False)
|
|
405
|
+
if dry_run:
|
|
406
|
+
click.echo(
|
|
407
|
+
f"Would delete {self.metadata.resource_type.lower()} '{name}'",
|
|
408
|
+
err=True,
|
|
409
|
+
)
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
if delete_config.confirmation_required:
|
|
413
|
+
confirm_flag = kwargs.get("confirm", False)
|
|
414
|
+
if not confirm_flag:
|
|
415
|
+
if delete_config.confirmation_prompt:
|
|
416
|
+
prompt = delete_config.confirmation_prompt.format(
|
|
417
|
+
resource=self.metadata.resource_type, identifier=name
|
|
418
|
+
)
|
|
419
|
+
else:
|
|
420
|
+
prompt = f"Delete {self.metadata.resource_type} '{name}'?"
|
|
421
|
+
|
|
422
|
+
if not click.confirm(prompt):
|
|
423
|
+
click.echo("Deletion cancelled.")
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
service.delete(name=name, folder_path=folder_path, folder_key=folder_key)
|
|
427
|
+
|
|
428
|
+
click.echo(
|
|
429
|
+
f"Deleted {self.metadata.resource_type.lower()} '{name}'", err=True
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
delete_cmd.help = f"""Delete a {self.metadata.resource_type}.
|
|
433
|
+
|
|
434
|
+
Examples:
|
|
435
|
+
uipath {self.metadata.service_name} delete my-resource --confirm
|
|
436
|
+
uipath {self.metadata.service_name} delete my-resource --dry-run
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
self._track_command("delete", delete_cmd)
|
|
440
|
+
|
|
441
|
+
def _add_exists_command(self, group: click.Group) -> None:
|
|
442
|
+
"""Add exists command to the group (if service supports it).
|
|
443
|
+
|
|
444
|
+
Generates: uipath <service> exists <name> [--folder-path PATH]
|
|
445
|
+
|
|
446
|
+
Note:
|
|
447
|
+
This command is only added to the CLI. The actual validation
|
|
448
|
+
of whether the service implements exists() happens at runtime.
|
|
449
|
+
"""
|
|
450
|
+
exists_config = self.metadata.exists_cmd
|
|
451
|
+
identifier = exists_config.identifier_arg_name
|
|
452
|
+
|
|
453
|
+
@group.command("exists")
|
|
454
|
+
@click.argument(identifier.upper())
|
|
455
|
+
@common_service_options
|
|
456
|
+
@service_command
|
|
457
|
+
def exists_cmd(
|
|
458
|
+
ctx: click.Context,
|
|
459
|
+
folder_path: str | None,
|
|
460
|
+
folder_key: str | None,
|
|
461
|
+
format: str | None,
|
|
462
|
+
output: str | None,
|
|
463
|
+
**kwargs: Any,
|
|
464
|
+
) -> bool | dict[str, Any] | None:
|
|
465
|
+
"""Check if a resource exists."""
|
|
466
|
+
client = ServiceCommandBase.get_client(ctx)
|
|
467
|
+
service = getattr(client, self.metadata.service_attr)
|
|
468
|
+
|
|
469
|
+
if not has_exists_method(service):
|
|
470
|
+
click.echo(
|
|
471
|
+
f"Error: {self.metadata.resource_type} service does not support exists command",
|
|
472
|
+
err=True,
|
|
473
|
+
)
|
|
474
|
+
raise click.Abort()
|
|
475
|
+
|
|
476
|
+
identifier_value = kwargs.get(identifier)
|
|
477
|
+
|
|
478
|
+
exists = service.exists(
|
|
479
|
+
identifier_value, folder_path=folder_path, folder_key=folder_key
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
if exists_config.return_format == "bool":
|
|
483
|
+
return exists
|
|
484
|
+
elif exists_config.return_format == "dict":
|
|
485
|
+
return {"exists": exists, identifier: identifier_value}
|
|
486
|
+
else: # text
|
|
487
|
+
if exists:
|
|
488
|
+
click.echo(
|
|
489
|
+
f"{self.metadata.resource_type} '{identifier_value}' exists"
|
|
490
|
+
)
|
|
491
|
+
else:
|
|
492
|
+
click.echo(
|
|
493
|
+
f"{self.metadata.resource_type} '{identifier_value}' does not exist"
|
|
494
|
+
)
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
exists_cmd.help = f"""Check if a {self.metadata.resource_type} exists.
|
|
498
|
+
|
|
499
|
+
Examples:
|
|
500
|
+
uipath {self.metadata.service_name} exists my-resource
|
|
501
|
+
uipath {self.metadata.service_name} exists my-resource --folder-path Shared
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
self._track_command("exists", exists_cmd)
|
|
505
|
+
|
|
506
|
+
def _track_command(self, command_name: str, command: Callable[..., Any]) -> None:
|
|
507
|
+
"""Track generated command for provenance.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
command_name: Name of the command (e.g., "list", "create")
|
|
511
|
+
command: The Click command function
|
|
512
|
+
"""
|
|
513
|
+
self._command_names.add(command_name)
|
|
514
|
+
|
|
515
|
+
func_name = command_name
|
|
516
|
+
if hasattr(command, "callback") and hasattr(command.callback, "__name__"):
|
|
517
|
+
func_name = command.callback.__name__
|
|
518
|
+
elif hasattr(command, "__name__"):
|
|
519
|
+
func_name = command.__name__
|
|
520
|
+
|
|
521
|
+
provenance: ProvenanceDict = {
|
|
522
|
+
"generator": GeneratorType.AUTO.value,
|
|
523
|
+
"service_name": self.metadata.service_name,
|
|
524
|
+
"resource_type": self.metadata.resource_type,
|
|
525
|
+
"command_function": func_name,
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
self._provenance[command_name] = provenance
|
|
529
|
+
setattr(command, "__provenance__", provenance) # noqa: B010 # Dynamic attribute not in type
|
|
530
|
+
|
|
531
|
+
def get_command_names(self) -> set[str]:
|
|
532
|
+
"""Get set of generated command names.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Set of command names (e.g., {"list", "retrieve", "create", "delete", "exists"})
|
|
536
|
+
"""
|
|
537
|
+
return self._command_names.copy()
|
|
538
|
+
|
|
539
|
+
def get_provenance(self, command_name: str) -> ProvenanceDict | None:
|
|
540
|
+
"""Get provenance information for a generated command.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
command_name: Name of the command
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
Provenance dict or None if command not found
|
|
547
|
+
"""
|
|
548
|
+
return self._provenance.get(command_name)
|
|
549
|
+
|
|
550
|
+
def add_nested_group(
|
|
551
|
+
self, service_group: click.Group, group_name: str, custom_group: click.Group
|
|
552
|
+
) -> None:
|
|
553
|
+
"""Add a custom nested command group to the service group.
|
|
554
|
+
|
|
555
|
+
This allows adding custom operations that don't fit the CRUD pattern,
|
|
556
|
+
such as the buckets file commands (upload, download, list-files, etc.).
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
service_group: The service group created by register()
|
|
560
|
+
group_name: Name for the nested group (e.g., "file")
|
|
561
|
+
custom_group: The Click group with custom commands
|
|
562
|
+
|
|
563
|
+
Raises:
|
|
564
|
+
ValueError: If group_name collides with existing command
|
|
565
|
+
|
|
566
|
+
Example:
|
|
567
|
+
>>> @click.group()
|
|
568
|
+
... def file_group():
|
|
569
|
+
... '''File operations'''
|
|
570
|
+
... pass
|
|
571
|
+
>>>
|
|
572
|
+
>>> @file_group.command("upload")
|
|
573
|
+
... def upload_cmd():
|
|
574
|
+
... '''Upload a file'''
|
|
575
|
+
... pass
|
|
576
|
+
>>>
|
|
577
|
+
>>> generator = ServiceCLIGenerator(metadata)
|
|
578
|
+
>>> service_group = generator.register(parent_group)
|
|
579
|
+
>>> generator.add_nested_group(service_group, "file", file_group)
|
|
580
|
+
"""
|
|
581
|
+
if group_name in service_group.commands:
|
|
582
|
+
existing_provenance = self.get_provenance(group_name)
|
|
583
|
+
if existing_provenance:
|
|
584
|
+
raise ValueError(
|
|
585
|
+
f"Cannot add nested group '{group_name}': "
|
|
586
|
+
f"conflicts with auto-generated command '{group_name}' "
|
|
587
|
+
f"(generator: {existing_provenance['generator']})"
|
|
588
|
+
)
|
|
589
|
+
else:
|
|
590
|
+
raise ValueError(
|
|
591
|
+
f"Cannot add nested group '{group_name}': "
|
|
592
|
+
f"command name already exists"
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
service_group.add_command(custom_group, name=group_name)
|
|
596
|
+
|
|
597
|
+
self._command_names.add(group_name)
|
|
598
|
+
provenance: ProvenanceDict = {
|
|
599
|
+
"generator": GeneratorType.MANUAL.value,
|
|
600
|
+
"type": "nested_group",
|
|
601
|
+
"service_name": self.metadata.service_name,
|
|
602
|
+
"command_function": group_name,
|
|
603
|
+
}
|
|
604
|
+
self._provenance[group_name] = provenance
|
|
605
|
+
setattr(custom_group, "__provenance__", provenance) # noqa: B010 # Dynamic attribute not in type
|
|
606
|
+
|
|
607
|
+
def override_command(
|
|
608
|
+
self,
|
|
609
|
+
service_group: click.Group,
|
|
610
|
+
command_name: str,
|
|
611
|
+
custom_command: click.Command,
|
|
612
|
+
) -> None:
|
|
613
|
+
"""Override an auto-generated command with a custom implementation.
|
|
614
|
+
|
|
615
|
+
This allows replacing auto-generated CRUD commands with custom logic
|
|
616
|
+
while preserving the same command name and interface.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
service_group: The service group created by register()
|
|
620
|
+
command_name: Name of the command to override (e.g., "list", "create")
|
|
621
|
+
custom_command: The custom Click command
|
|
622
|
+
|
|
623
|
+
Raises:
|
|
624
|
+
ValueError: If command_name doesn't exist or wasn't auto-generated
|
|
625
|
+
|
|
626
|
+
Example:
|
|
627
|
+
>>> @click.command("list")
|
|
628
|
+
... def custom_list():
|
|
629
|
+
... '''Custom list with extra features'''
|
|
630
|
+
... pass
|
|
631
|
+
>>>
|
|
632
|
+
>>> generator = ServiceCLIGenerator(metadata)
|
|
633
|
+
>>> service_group = generator.register(parent_group)
|
|
634
|
+
>>> generator.override_command(service_group, "list", custom_list)
|
|
635
|
+
"""
|
|
636
|
+
if command_name not in service_group.commands:
|
|
637
|
+
raise ValueError(
|
|
638
|
+
f"Cannot override command '{command_name}': command does not exist"
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
provenance = self.get_provenance(command_name)
|
|
642
|
+
if not provenance or provenance.get("generator") != GeneratorType.AUTO.value:
|
|
643
|
+
raise ValueError(
|
|
644
|
+
f"Cannot override command '{command_name}': "
|
|
645
|
+
f"not an auto-generated command (only auto-generated commands can be overridden)"
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
del service_group.commands[command_name]
|
|
649
|
+
service_group.add_command(custom_command, name=command_name)
|
|
650
|
+
|
|
651
|
+
new_provenance: ProvenanceDict = {
|
|
652
|
+
"generator": GeneratorType.OVERRIDE.value,
|
|
653
|
+
"original_generator": provenance["generator"],
|
|
654
|
+
"service_name": self.metadata.service_name,
|
|
655
|
+
"resource_type": self.metadata.resource_type,
|
|
656
|
+
"command_function": custom_command.callback.__name__
|
|
657
|
+
if hasattr(custom_command, "callback")
|
|
658
|
+
and custom_command.callback is not None
|
|
659
|
+
else command_name,
|
|
660
|
+
"overridden_at": "user_custom",
|
|
661
|
+
}
|
|
662
|
+
self._provenance[command_name] = new_provenance
|
|
663
|
+
setattr(custom_command, "__provenance__", new_provenance) # noqa: B010 # Dynamic attribute not in type
|
|
664
|
+
|
|
665
|
+
def validate_no_collisions(
|
|
666
|
+
self, service_group: click.Group, additional_commands: list[str]
|
|
667
|
+
) -> list[str]:
|
|
668
|
+
"""Validate that additional commands won't collide with generated ones.
|
|
669
|
+
|
|
670
|
+
This is useful for checking naming conflicts before adding custom commands.
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
service_group: The service group created by register()
|
|
674
|
+
additional_commands: List of command names to check
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
List of collision errors (empty if no collisions)
|
|
678
|
+
|
|
679
|
+
Example:
|
|
680
|
+
>>> generator = ServiceCLIGenerator(metadata)
|
|
681
|
+
>>> service_group = generator.register(parent_group)
|
|
682
|
+
>>> errors = generator.validate_no_collisions(service_group, ["list", "custom"])
|
|
683
|
+
>>> if errors:
|
|
684
|
+
... for error in errors:
|
|
685
|
+
... print(error)
|
|
686
|
+
"""
|
|
687
|
+
collisions = []
|
|
688
|
+
|
|
689
|
+
for cmd_name in additional_commands:
|
|
690
|
+
if cmd_name in service_group.commands:
|
|
691
|
+
provenance = self.get_provenance(cmd_name)
|
|
692
|
+
if provenance:
|
|
693
|
+
collisions.append(
|
|
694
|
+
f"Command '{cmd_name}' conflicts with auto-generated command "
|
|
695
|
+
f"(generator: {provenance.get('generator', 'unknown')})"
|
|
696
|
+
)
|
|
697
|
+
else:
|
|
698
|
+
collisions.append(
|
|
699
|
+
f"Command '{cmd_name}' conflicts with existing command"
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
return collisions
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
__all__ = ["ServiceCLIGenerator"]
|