uipath 2.1.130__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/_auth/auth_config_cloud.json +1 -1
- 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.130.dist-info → uipath-2.1.132.dist-info}/METADATA +1 -1
- {uipath-2.1.130.dist-info → uipath-2.1.132.dist-info}/RECORD +22 -11
- {uipath-2.1.130.dist-info → uipath-2.1.132.dist-info}/WHEEL +0 -0
- {uipath-2.1.130.dist-info → uipath-2.1.132.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.130.dist-info → uipath-2.1.132.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Service metadata models using Pydantic.
|
|
2
|
+
|
|
3
|
+
This module defines the metadata structure for CLI service command generation.
|
|
4
|
+
All models are immutable (frozen) to prevent accidental modification after creation.
|
|
5
|
+
|
|
6
|
+
The metadata describes:
|
|
7
|
+
- Service identification (name, resource type)
|
|
8
|
+
- CRUD operation parameters
|
|
9
|
+
- Command configuration (confirmation, dry-run support)
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from ._service_metadata import ServiceMetadata, CreateParameter
|
|
13
|
+
>>>
|
|
14
|
+
>>> BUCKETS_METADATA = ServiceMetadata(
|
|
15
|
+
... service_name="buckets",
|
|
16
|
+
... service_attr="buckets",
|
|
17
|
+
... resource_type="Bucket",
|
|
18
|
+
... create_params={
|
|
19
|
+
... "description": CreateParameter(
|
|
20
|
+
... type="str",
|
|
21
|
+
... required=False,
|
|
22
|
+
... help="Bucket description",
|
|
23
|
+
... )
|
|
24
|
+
... },
|
|
25
|
+
... )
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from pydantic import BaseModel, Field, model_validator
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CreateParameter(BaseModel):
|
|
34
|
+
"""Metadata for a single create command parameter.
|
|
35
|
+
|
|
36
|
+
This defines how a parameter should appear in the CLI command.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
type: Type name from TYPE_REGISTRY (e.g., "str", "int", "bool", "float")
|
|
40
|
+
required: Whether the parameter is required (default: False)
|
|
41
|
+
help: Help text shown in --help output
|
|
42
|
+
default: Default value if not provided by user
|
|
43
|
+
option_name: CLI option name (default: derived from field name)
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> description_param = CreateParameter(
|
|
47
|
+
... type="str",
|
|
48
|
+
... required=False,
|
|
49
|
+
... help="Resource description",
|
|
50
|
+
... default=None,
|
|
51
|
+
... )
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
type: str
|
|
55
|
+
required: bool = False
|
|
56
|
+
help: str = ""
|
|
57
|
+
default: Any = None
|
|
58
|
+
option_name: str | None = None
|
|
59
|
+
|
|
60
|
+
class Config:
|
|
61
|
+
"""Pydantic configuration."""
|
|
62
|
+
|
|
63
|
+
frozen = True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DeleteCommandConfig(BaseModel):
|
|
67
|
+
"""Configuration for delete command behavior.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
confirmation_required: Whether to require --confirm flag (default: True)
|
|
71
|
+
dry_run_supported: Whether to support --dry-run flag (default: True)
|
|
72
|
+
confirmation_prompt: Custom confirmation prompt template
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
>>> delete_config = DeleteCommandConfig(
|
|
76
|
+
... confirmation_required=True,
|
|
77
|
+
... dry_run_supported=True,
|
|
78
|
+
... confirmation_prompt="Delete {resource} '{identifier}'?",
|
|
79
|
+
... )
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
confirmation_required: bool = True
|
|
83
|
+
dry_run_supported: bool = True
|
|
84
|
+
confirmation_prompt: str | None = None
|
|
85
|
+
|
|
86
|
+
class Config:
|
|
87
|
+
"""Pydantic configuration."""
|
|
88
|
+
|
|
89
|
+
frozen = True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ExistsCommandConfig(BaseModel):
|
|
93
|
+
"""Configuration for exists command behavior.
|
|
94
|
+
|
|
95
|
+
Attributes:
|
|
96
|
+
identifier_arg_name: Name of the identifier argument (default: "name")
|
|
97
|
+
return_format: Format of the return value ("bool", "dict", "text")
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
>>> exists_config = ExistsCommandConfig(
|
|
101
|
+
... identifier_arg_name="key",
|
|
102
|
+
... return_format="dict",
|
|
103
|
+
... )
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
identifier_arg_name: str = "name"
|
|
107
|
+
return_format: str = "dict" # "bool", "dict", or "text"
|
|
108
|
+
|
|
109
|
+
class Config:
|
|
110
|
+
"""Pydantic configuration."""
|
|
111
|
+
|
|
112
|
+
frozen = True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ServiceMetadata(BaseModel):
|
|
116
|
+
"""Complete metadata for a service's CLI commands.
|
|
117
|
+
|
|
118
|
+
This metadata is used by ServiceCLIGenerator to auto-generate standard
|
|
119
|
+
CRUD commands (list, retrieve, create, delete, exists).
|
|
120
|
+
|
|
121
|
+
Attributes:
|
|
122
|
+
service_name: CLI command group name (e.g., "buckets", "assets")
|
|
123
|
+
service_attr: Attribute name on UiPath client (e.g., client.buckets)
|
|
124
|
+
resource_type: Human-readable resource name (e.g., "Bucket", "Asset")
|
|
125
|
+
resource_plural: Plural form of resource type (auto-generated if not provided)
|
|
126
|
+
create_params: Dictionary of parameters for create command
|
|
127
|
+
delete_cmd: Configuration for delete command behavior
|
|
128
|
+
exists_cmd: Configuration for exists command behavior
|
|
129
|
+
list_supports_filters: Whether list command supports additional filters
|
|
130
|
+
retrieve_identifier: Name of the identifier argument for retrieve
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
>>> metadata = ServiceMetadata(
|
|
134
|
+
... service_name="buckets",
|
|
135
|
+
... service_attr="buckets",
|
|
136
|
+
... resource_type="Bucket",
|
|
137
|
+
... create_params={
|
|
138
|
+
... "description": CreateParameter(
|
|
139
|
+
... type="str",
|
|
140
|
+
... required=False,
|
|
141
|
+
... help="Bucket description",
|
|
142
|
+
... )
|
|
143
|
+
... },
|
|
144
|
+
... )
|
|
145
|
+
>>> metadata.resource_plural
|
|
146
|
+
'Buckets'
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
service_name: str
|
|
150
|
+
service_attr: str
|
|
151
|
+
resource_type: str
|
|
152
|
+
resource_plural: str | None = None
|
|
153
|
+
create_params: dict[str, CreateParameter] = Field(default_factory=dict)
|
|
154
|
+
delete_cmd: DeleteCommandConfig = Field(default_factory=DeleteCommandConfig)
|
|
155
|
+
exists_cmd: ExistsCommandConfig = Field(default_factory=ExistsCommandConfig)
|
|
156
|
+
list_supports_filters: bool = False
|
|
157
|
+
retrieve_identifier: str = "name"
|
|
158
|
+
|
|
159
|
+
class Config:
|
|
160
|
+
"""Pydantic configuration."""
|
|
161
|
+
|
|
162
|
+
frozen = True
|
|
163
|
+
|
|
164
|
+
@model_validator(mode="after")
|
|
165
|
+
def set_defaults(self) -> "ServiceMetadata":
|
|
166
|
+
"""Set default values that depend on other fields.
|
|
167
|
+
|
|
168
|
+
This validator runs after model creation to set derived defaults:
|
|
169
|
+
- resource_plural from resource_type
|
|
170
|
+
- delete_cmd defaults if not explicitly set
|
|
171
|
+
|
|
172
|
+
Note:
|
|
173
|
+
Since the model is frozen, we use object.__setattr__ to modify fields.
|
|
174
|
+
"""
|
|
175
|
+
if self.resource_plural is None:
|
|
176
|
+
object.__setattr__(self, "resource_plural", f"{self.resource_type}s")
|
|
177
|
+
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
def validate_types(self) -> None:
|
|
181
|
+
"""Validate that all parameter types are in TYPE_REGISTRY.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
ValueError: If any parameter type is not registered
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
>>> metadata = ServiceMetadata(
|
|
188
|
+
... service_name="test",
|
|
189
|
+
... service_attr="test",
|
|
190
|
+
... resource_type="Test",
|
|
191
|
+
... create_params={
|
|
192
|
+
... "invalid": CreateParameter(type="InvalidType", required=False)
|
|
193
|
+
... },
|
|
194
|
+
... )
|
|
195
|
+
>>> metadata.validate_types()
|
|
196
|
+
Traceback (most recent call last):
|
|
197
|
+
...
|
|
198
|
+
ValueError: Invalid type 'InvalidType' for parameter 'invalid' in service 'test'...
|
|
199
|
+
"""
|
|
200
|
+
from ._type_registry import is_valid_type
|
|
201
|
+
|
|
202
|
+
for param_name, param in self.create_params.items():
|
|
203
|
+
if not is_valid_type(param.type):
|
|
204
|
+
from ._type_registry import TYPE_REGISTRY
|
|
205
|
+
|
|
206
|
+
valid_types = ", ".join(sorted(TYPE_REGISTRY.keys()))
|
|
207
|
+
raise ValueError(
|
|
208
|
+
f"Invalid type '{param.type}' for parameter '{param_name}' "
|
|
209
|
+
f"in service '{self.service_name}'. Valid types: {valid_types}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
__all__ = [
|
|
214
|
+
"CreateParameter",
|
|
215
|
+
"DeleteCommandConfig",
|
|
216
|
+
"ExistsCommandConfig",
|
|
217
|
+
"ServiceMetadata",
|
|
218
|
+
]
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Protocol definition for CRUD services.
|
|
2
|
+
|
|
3
|
+
This module defines the CRUDServiceProtocol that services must implement
|
|
4
|
+
to be compatible with the CLI command generator.
|
|
5
|
+
|
|
6
|
+
The protocol uses Python's Protocol (PEP 544) for structural subtyping,
|
|
7
|
+
allowing duck-typing without requiring explicit inheritance.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from typing import Protocol, runtime_checkable
|
|
11
|
+
>>> from ._service_protocol import CRUDServiceProtocol
|
|
12
|
+
>>>
|
|
13
|
+
>>> # A service that implements the protocol
|
|
14
|
+
>>> class BucketsService:
|
|
15
|
+
... def list(self, folder_path=None, folder_key=None):
|
|
16
|
+
... return iter([])
|
|
17
|
+
...
|
|
18
|
+
... def retrieve(self, name, folder_path=None, folder_key=None):
|
|
19
|
+
... return {"name": name}
|
|
20
|
+
...
|
|
21
|
+
... # ... other CRUD methods
|
|
22
|
+
>>>
|
|
23
|
+
>>> # No explicit inheritance needed - structural typing
|
|
24
|
+
>>> def use_service(service: CRUDServiceProtocol):
|
|
25
|
+
... return list(service.list())
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from typing import Any, Iterator, Protocol, runtime_checkable
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class CRUDServiceProtocol(Protocol):
|
|
33
|
+
"""Protocol for services that support standard CRUD operations.
|
|
34
|
+
|
|
35
|
+
Services implementing this protocol can use the ServiceCLIGenerator
|
|
36
|
+
to automatically generate CLI commands.
|
|
37
|
+
|
|
38
|
+
All methods support folder-scoped operations via folder_path or folder_key
|
|
39
|
+
parameters. This matches the UiPath platform's hierarchical folder structure.
|
|
40
|
+
|
|
41
|
+
Required Methods:
|
|
42
|
+
list: List all resources (returns iterator)
|
|
43
|
+
retrieve: Get a single resource by identifier
|
|
44
|
+
create: Create a new resource
|
|
45
|
+
delete: Delete a resource by identifier
|
|
46
|
+
|
|
47
|
+
Optional Methods (NOT in protocol, checked separately):
|
|
48
|
+
exists: Check if a resource exists (will be used if present on service)
|
|
49
|
+
|
|
50
|
+
Note:
|
|
51
|
+
This is a Protocol, not a base class. Services don't need to inherit
|
|
52
|
+
from it; they just need to implement the required methods with
|
|
53
|
+
compatible signatures.
|
|
54
|
+
|
|
55
|
+
The exists() method is NOT part of the protocol to allow optional
|
|
56
|
+
implementation. Use hasattr(service, 'exists') to check for it.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def list(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
folder_path: str | None = None,
|
|
63
|
+
folder_key: str | None = None,
|
|
64
|
+
**kwargs: Any,
|
|
65
|
+
) -> Iterator[Any]:
|
|
66
|
+
"""List all resources in the specified folder.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
folder_path: Folder path (e.g., "Shared")
|
|
70
|
+
folder_key: Folder UUID key
|
|
71
|
+
**kwargs: Additional service-specific parameters
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Iterator of resource objects
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
>>> buckets_service.list(folder_path="Shared")
|
|
78
|
+
<iterator of Bucket objects>
|
|
79
|
+
"""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
def retrieve(
|
|
83
|
+
self,
|
|
84
|
+
name: str,
|
|
85
|
+
*,
|
|
86
|
+
folder_path: str | None = None,
|
|
87
|
+
folder_key: str | None = None,
|
|
88
|
+
**kwargs: Any,
|
|
89
|
+
) -> Any:
|
|
90
|
+
"""Retrieve a single resource by identifier.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
name: Resource identifier (usually name, but could be key)
|
|
94
|
+
folder_path: Folder path (e.g., "Shared")
|
|
95
|
+
folder_key: Folder UUID key
|
|
96
|
+
**kwargs: Additional service-specific parameters
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Resource object
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
LookupError: If resource not found
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> buckets_service.retrieve("my-bucket", folder_path="Shared")
|
|
106
|
+
Bucket(name="my-bucket", ...)
|
|
107
|
+
"""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
def create(
|
|
111
|
+
self,
|
|
112
|
+
name: str,
|
|
113
|
+
*,
|
|
114
|
+
folder_path: str | None = None,
|
|
115
|
+
folder_key: str | None = None,
|
|
116
|
+
**kwargs: Any,
|
|
117
|
+
) -> Any:
|
|
118
|
+
"""Create a new resource.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
name: Resource name
|
|
122
|
+
folder_path: Folder path (e.g., "Shared")
|
|
123
|
+
folder_key: Folder UUID key
|
|
124
|
+
**kwargs: Additional service-specific parameters (from metadata)
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Created resource object
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> buckets_service.create(
|
|
131
|
+
... "my-bucket",
|
|
132
|
+
... folder_path="Shared",
|
|
133
|
+
... description="My bucket"
|
|
134
|
+
... )
|
|
135
|
+
Bucket(name="my-bucket", description="My bucket", ...)
|
|
136
|
+
"""
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
def delete(
|
|
140
|
+
self,
|
|
141
|
+
name: str,
|
|
142
|
+
*,
|
|
143
|
+
folder_path: str | None = None,
|
|
144
|
+
folder_key: str | None = None,
|
|
145
|
+
**kwargs: Any,
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Delete a resource by identifier.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
name: Resource identifier to delete
|
|
151
|
+
folder_path: Folder path (e.g., "Shared")
|
|
152
|
+
folder_key: Folder UUID key
|
|
153
|
+
**kwargs: Additional service-specific parameters
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
LookupError: If resource not found
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> buckets_service.delete("my-bucket", folder_path="Shared")
|
|
160
|
+
"""
|
|
161
|
+
...
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def has_exists_method(service: Any) -> bool:
|
|
165
|
+
"""Check if a service has the optional exists() method.
|
|
166
|
+
|
|
167
|
+
This is separate from the Protocol because exists() is optional.
|
|
168
|
+
Not all services need to implement it.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
service: Service instance to check
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
True if service has a callable exists() method
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
>>> class ServiceWithExists:
|
|
178
|
+
... def exists(self, name, **kwargs):
|
|
179
|
+
... return True
|
|
180
|
+
>>>
|
|
181
|
+
>>> service = ServiceWithExists()
|
|
182
|
+
>>> has_exists_method(service)
|
|
183
|
+
True
|
|
184
|
+
"""
|
|
185
|
+
return hasattr(service, "exists") and callable(service.exists)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def validate_service_protocol(service: Any, service_name: str) -> None:
|
|
189
|
+
"""Validate that a service implements the CRUDServiceProtocol.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
service: Service instance to validate
|
|
193
|
+
service_name: Name of the service for error messages
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
TypeError: If service doesn't implement required methods
|
|
197
|
+
|
|
198
|
+
Example:
|
|
199
|
+
>>> class IncompleteService:
|
|
200
|
+
... def list(self):
|
|
201
|
+
... pass
|
|
202
|
+
... # Missing other methods
|
|
203
|
+
>>>
|
|
204
|
+
>>> validate_service_protocol(IncompleteService(), "incomplete")
|
|
205
|
+
Traceback (most recent call last):
|
|
206
|
+
...
|
|
207
|
+
TypeError: Service 'incomplete' must implement: retrieve, create, delete
|
|
208
|
+
"""
|
|
209
|
+
required_methods = ["list", "retrieve", "create", "delete"]
|
|
210
|
+
missing_methods = []
|
|
211
|
+
for method_name in required_methods:
|
|
212
|
+
if not hasattr(service, method_name) or not callable(
|
|
213
|
+
getattr(service, method_name)
|
|
214
|
+
):
|
|
215
|
+
missing_methods.append(method_name)
|
|
216
|
+
|
|
217
|
+
if missing_methods:
|
|
218
|
+
raise TypeError(
|
|
219
|
+
f"Service '{service_name}' must implement: {', '.join(missing_methods)}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
__all__ = ["CRUDServiceProtocol", "validate_service_protocol", "has_exists_method"]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Type registry for safe parameter type resolution.
|
|
2
|
+
|
|
3
|
+
This module provides a safe way to resolve type strings to Python types,
|
|
4
|
+
replacing the use of eval() which is a security vulnerability.
|
|
5
|
+
|
|
6
|
+
The TYPE_REGISTRY is used by the ServiceMetadata system to convert type
|
|
7
|
+
strings (e.g., "str", "int") to actual Python types for Click parameter
|
|
8
|
+
validation.
|
|
9
|
+
|
|
10
|
+
Security Note:
|
|
11
|
+
This registry approach prevents arbitrary code execution that would be
|
|
12
|
+
possible with eval(). Only explicitly registered types can be used.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> from ._type_registry import get_type
|
|
16
|
+
>>> str_type = get_type("str")
|
|
17
|
+
>>> str_type is str
|
|
18
|
+
True
|
|
19
|
+
>>> get_type("InvalidType") # Raises ValueError
|
|
20
|
+
ValueError: Unknown type: 'InvalidType'. Valid types: bool, float, int, str
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import Any, Type
|
|
24
|
+
|
|
25
|
+
TYPE_REGISTRY: dict[str, Type[Any]] = {
|
|
26
|
+
"str": str,
|
|
27
|
+
"int": int,
|
|
28
|
+
"bool": bool,
|
|
29
|
+
"float": float,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_type(type_name: str) -> Type[Any]:
|
|
34
|
+
"""Get Python type from string name safely.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
type_name: String name of the type (e.g., "str", "int", "bool", "float")
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The Python type corresponding to the type name
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
ValueError: If type_name is not in the registry
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> get_type("str")
|
|
47
|
+
<class 'str'>
|
|
48
|
+
>>> get_type("int")
|
|
49
|
+
<class 'int'>
|
|
50
|
+
>>> get_type("InvalidType")
|
|
51
|
+
Traceback (most recent call last):
|
|
52
|
+
...
|
|
53
|
+
ValueError: Unknown type: 'InvalidType'. Valid types: bool, float, int, str
|
|
54
|
+
"""
|
|
55
|
+
param_type = TYPE_REGISTRY.get(type_name)
|
|
56
|
+
if param_type is None:
|
|
57
|
+
valid_types = ", ".join(sorted(TYPE_REGISTRY.keys()))
|
|
58
|
+
raise ValueError(f"Unknown type: '{type_name}'. Valid types: {valid_types}")
|
|
59
|
+
return param_type
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def register_type(type_name: str, type_class: Type[Any]) -> None:
|
|
63
|
+
"""Register a new type in the registry.
|
|
64
|
+
|
|
65
|
+
This function allows extending the registry with custom types if needed.
|
|
66
|
+
Use with caution and only register types that are safe for CLI parameters.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
type_name: String name for the type
|
|
70
|
+
type_class: The Python type class
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValueError: If type_name already exists in registry
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> from pathlib import Path
|
|
77
|
+
>>> register_type("path", Path)
|
|
78
|
+
>>> get_type("path")
|
|
79
|
+
<class 'pathlib.Path'>
|
|
80
|
+
"""
|
|
81
|
+
if type_name in TYPE_REGISTRY:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"Type '{type_name}' is already registered as {TYPE_REGISTRY[type_name]}"
|
|
84
|
+
)
|
|
85
|
+
TYPE_REGISTRY[type_name] = type_class
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def is_valid_type(type_name: str) -> bool:
|
|
89
|
+
"""Check if a type name is registered.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
type_name: String name to check
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True if type is registered, False otherwise
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
>>> is_valid_type("str")
|
|
99
|
+
True
|
|
100
|
+
>>> is_valid_type("InvalidType")
|
|
101
|
+
False
|
|
102
|
+
"""
|
|
103
|
+
return type_name in TYPE_REGISTRY
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
__all__ = ["TYPE_REGISTRY", "get_type", "register_type", "is_valid_type"]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Common validators for CLI commands.
|
|
2
|
+
|
|
3
|
+
This module provides reusable validation functions for common CLI inputs
|
|
4
|
+
like folder paths, UUIDs, and resource names.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_folder_path(ctx, param, value: Optional[str]) -> Optional[str]:
|
|
14
|
+
"""Validate folder path format.
|
|
15
|
+
|
|
16
|
+
Folder paths should be in the format: "Folder1/Subfolder2"
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
ctx: Click context
|
|
20
|
+
param: Click parameter
|
|
21
|
+
value: Folder path to validate
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Validated folder path
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
click.BadParameter: If folder path is invalid
|
|
28
|
+
"""
|
|
29
|
+
if value is None:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
if value == "":
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
if value.startswith("/") or value.endswith("/"):
|
|
36
|
+
raise click.BadParameter("Folder path should not start or end with '/'")
|
|
37
|
+
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_uuid(ctx, param, value: Optional[str]) -> Optional[str]:
|
|
42
|
+
"""Validate UUID format.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
ctx: Click context
|
|
46
|
+
param: Click parameter
|
|
47
|
+
value: UUID to validate
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Validated UUID
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
click.BadParameter: If UUID is invalid
|
|
54
|
+
"""
|
|
55
|
+
if value is None:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
# UUID regex pattern
|
|
59
|
+
uuid_pattern = re.compile(
|
|
60
|
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if not uuid_pattern.match(value):
|
|
64
|
+
raise click.BadParameter(
|
|
65
|
+
f"'{value}' is not a valid UUID. "
|
|
66
|
+
"Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def validate_resource_name(ctx, param, value: Optional[str]) -> Optional[str]:
|
|
73
|
+
"""Validate resource name.
|
|
74
|
+
|
|
75
|
+
Resource names should:
|
|
76
|
+
- Not be empty
|
|
77
|
+
- Not contain special characters that might cause issues
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
ctx: Click context
|
|
81
|
+
param: Click parameter
|
|
82
|
+
value: Resource name to validate
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Validated resource name
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
click.BadParameter: If resource name is invalid
|
|
89
|
+
"""
|
|
90
|
+
if value is None:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
if not value.strip():
|
|
94
|
+
raise click.BadParameter("Resource name cannot be empty")
|
|
95
|
+
|
|
96
|
+
invalid_chars = ["<", ">", ":", '"', "|", "?", "*"]
|
|
97
|
+
for char in invalid_chars:
|
|
98
|
+
if char in value:
|
|
99
|
+
raise click.BadParameter(
|
|
100
|
+
f"Resource name contains invalid character: '{char}'"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def validate_mutually_exclusive(ctx, param, value, exclusive_with: str):
|
|
107
|
+
"""Validate that two options are mutually exclusive.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
ctx: Click context
|
|
111
|
+
param: Click parameter
|
|
112
|
+
value: Current parameter value (can be any type)
|
|
113
|
+
exclusive_with: Name of the mutually exclusive parameter
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Validated value
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
click.UsageError: If both parameters are provided
|
|
120
|
+
"""
|
|
121
|
+
if value is not None and ctx.params.get(exclusive_with) is not None:
|
|
122
|
+
raise click.UsageError(
|
|
123
|
+
f"--{param.name} and --{exclusive_with} are mutually exclusive. "
|
|
124
|
+
"Provide only one."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return value
|