uipath 2.1.4__py3-none-any.whl → 2.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uipath/_cli/__init__.py +2 -0
- uipath/_cli/_auth/auth_config.json +2 -2
- uipath/_cli/_utils/_constants.py +6 -0
- uipath/_cli/_utils/_project_files.py +361 -0
- uipath/_cli/_utils/_studio_project.py +138 -0
- uipath/_cli/_utils/_uv_helpers.py +114 -0
- uipath/_cli/cli_pack.py +42 -341
- uipath/_cli/cli_push.py +546 -0
- uipath/_services/llm_gateway_service.py +149 -13
- {uipath-2.1.4.dist-info → uipath-2.1.6.dist-info}/METADATA +1 -1
- {uipath-2.1.4.dist-info → uipath-2.1.6.dist-info}/RECORD +14 -10
- {uipath-2.1.4.dist-info → uipath-2.1.6.dist-info}/WHEEL +0 -0
- {uipath-2.1.4.dist-info → uipath-2.1.6.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.4.dist-info → uipath-2.1.6.dist-info}/licenses/LICENSE +0 -0
uipath/_cli/__init__.py
CHANGED
@@ -10,6 +10,7 @@ from .cli_invoke import invoke as invoke # type: ignore
|
|
10
10
|
from .cli_new import new as new # type: ignore
|
11
11
|
from .cli_pack import pack as pack # type: ignore
|
12
12
|
from .cli_publish import publish as publish # type: ignore
|
13
|
+
from .cli_push import push as push # type: ignore
|
13
14
|
from .cli_run import run as run # type: ignore
|
14
15
|
|
15
16
|
|
@@ -63,3 +64,4 @@ cli.add_command(run)
|
|
63
64
|
cli.add_command(deploy)
|
64
65
|
cli.add_command(auth)
|
65
66
|
cli.add_command(invoke)
|
67
|
+
cli.add_command(push)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"client_id": "36dea5b8-e8bb-423d-8e7b-c808df8f1c00",
|
3
3
|
"redirect_uri": "http://localhost:__PY_REPLACE_PORT__/oidc/login",
|
4
|
-
"scope": "offline_access OrchestratorApiUserAccess IdentityServerApi ConnectionService DataService DocumentUnderstanding EnterpriseContextService Directory JamJamApi LLMGateway LLMOps OMS RCS.FolderAuthorization TM.Projects TM.TestCases TM.Requirements TM.TestSets",
|
4
|
+
"scope": "offline_access OrchestratorApiUserAccess StudioWebBackend IdentityServerApi ConnectionService DataService DocumentUnderstanding EnterpriseContextService Directory JamJamApi LLMGateway LLMOps OMS RCS.FolderAuthorization TM.Projects TM.TestCases TM.Requirements TM.TestSets",
|
5
5
|
"port": 8104
|
6
|
-
}
|
6
|
+
}
|
uipath/_cli/_utils/_constants.py
CHANGED
@@ -0,0 +1,361 @@
|
|
1
|
+
# type: ignore
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import re
|
5
|
+
from typing import Any, Dict, Optional, Tuple
|
6
|
+
|
7
|
+
from pydantic import BaseModel
|
8
|
+
|
9
|
+
from .._utils._console import ConsoleLogger
|
10
|
+
from ._constants import is_binary_file
|
11
|
+
|
12
|
+
try:
|
13
|
+
import tomllib
|
14
|
+
except ImportError:
|
15
|
+
import tomli as tomllib
|
16
|
+
|
17
|
+
|
18
|
+
class FileInfo(BaseModel):
|
19
|
+
"""Information about a file to be included in the project.
|
20
|
+
|
21
|
+
Attributes:
|
22
|
+
file_path: The absolute path to the file
|
23
|
+
relative_path: The path relative to the project root
|
24
|
+
is_binary: Whether the file should be treated as binary
|
25
|
+
"""
|
26
|
+
|
27
|
+
file_path: str
|
28
|
+
relative_path: str
|
29
|
+
is_binary: bool
|
30
|
+
|
31
|
+
|
32
|
+
console = ConsoleLogger()
|
33
|
+
|
34
|
+
|
35
|
+
def get_project_config(directory: str) -> dict[str, str]:
|
36
|
+
"""Retrieve and combine project configuration from uipath.json and pyproject.toml.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
directory: The root directory containing the configuration files
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
dict[str, str]: Combined configuration including project name, description,
|
43
|
+
entry points, version, and authors
|
44
|
+
|
45
|
+
Raises:
|
46
|
+
SystemExit: If required configuration files are missing or invalid
|
47
|
+
"""
|
48
|
+
config_path = os.path.join(directory, "uipath.json")
|
49
|
+
toml_path = os.path.join(directory, "pyproject.toml")
|
50
|
+
|
51
|
+
if not os.path.isfile(config_path):
|
52
|
+
console.error("uipath.json not found, please run `uipath init`.")
|
53
|
+
if not os.path.isfile(toml_path):
|
54
|
+
console.error("pyproject.toml not found.")
|
55
|
+
|
56
|
+
with open(config_path, "r") as config_file:
|
57
|
+
config_data = json.load(config_file)
|
58
|
+
|
59
|
+
validate_config_structure(config_data)
|
60
|
+
|
61
|
+
toml_data = read_toml_project(toml_path)
|
62
|
+
|
63
|
+
return {
|
64
|
+
"project_name": toml_data["name"],
|
65
|
+
"description": toml_data["description"],
|
66
|
+
"entryPoints": config_data["entryPoints"],
|
67
|
+
"version": toml_data["version"],
|
68
|
+
"authors": toml_data["authors"],
|
69
|
+
"dependencies": toml_data.get("dependencies", {}),
|
70
|
+
}
|
71
|
+
|
72
|
+
|
73
|
+
def validate_config(config: dict[str, str]) -> None:
|
74
|
+
"""Validate the combined project configuration.
|
75
|
+
|
76
|
+
Checks for required fields and invalid characters in project name and description.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
config: The combined configuration dictionary from uipath.json and pyproject.toml
|
80
|
+
|
81
|
+
Raises:
|
82
|
+
SystemExit: If validation fails for any required field or contains invalid characters
|
83
|
+
"""
|
84
|
+
if not config["project_name"] or config["project_name"].strip() == "":
|
85
|
+
console.error(
|
86
|
+
"Project name cannot be empty. Please specify a name in pyproject.toml."
|
87
|
+
)
|
88
|
+
|
89
|
+
if not config["description"] or config["description"].strip() == "":
|
90
|
+
console.error(
|
91
|
+
"Project description cannot be empty. Please specify a description in pyproject.toml."
|
92
|
+
)
|
93
|
+
|
94
|
+
if not config["authors"] or config["authors"].strip() == "":
|
95
|
+
console.error(
|
96
|
+
'Project authors cannot be empty. Please specify authors in pyproject.toml:\n authors = [{ name = "John Doe" }]'
|
97
|
+
)
|
98
|
+
|
99
|
+
invalid_chars = ["&", "<", ">", '"', "'", ";"]
|
100
|
+
for char in invalid_chars:
|
101
|
+
if char in config["project_name"]:
|
102
|
+
console.error(f"Project name contains invalid character: '{char}'")
|
103
|
+
|
104
|
+
for char in invalid_chars:
|
105
|
+
if char in config["description"]:
|
106
|
+
console.error(f"Project description contains invalid character: '{char}'")
|
107
|
+
|
108
|
+
|
109
|
+
def validate_config_structure(config_data: dict[str, Any]) -> None:
|
110
|
+
"""Validate the structure of uipath.json configuration.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
config_data: The raw configuration data from uipath.json
|
114
|
+
|
115
|
+
Raises:
|
116
|
+
SystemExit: If required fields are missing from the configuration
|
117
|
+
"""
|
118
|
+
required_fields = ["entryPoints"]
|
119
|
+
for field in required_fields:
|
120
|
+
if field not in config_data:
|
121
|
+
console.error(f"uipath.json is missing the required field: {field}.")
|
122
|
+
|
123
|
+
|
124
|
+
def ensure_config_file(directory: str) -> None:
|
125
|
+
"""Check if uipath.json exists in the specified directory.
|
126
|
+
|
127
|
+
Args:
|
128
|
+
directory: The directory to check for uipath.json
|
129
|
+
|
130
|
+
Raises:
|
131
|
+
SystemExit: If uipath.json is not found in the directory
|
132
|
+
"""
|
133
|
+
if not os.path.isfile(os.path.join(directory, "uipath.json")):
|
134
|
+
console.error(
|
135
|
+
"uipath.json not found. Please run `uipath init` in the project directory."
|
136
|
+
)
|
137
|
+
|
138
|
+
|
139
|
+
def extract_dependencies_from_toml(project_data: Dict) -> Dict[str, str]:
|
140
|
+
"""Extract and parse dependencies from pyproject.toml project data.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
project_data: The "project" section from pyproject.toml
|
144
|
+
|
145
|
+
Returns:
|
146
|
+
Dictionary mapping package names to version specifiers
|
147
|
+
"""
|
148
|
+
dependencies = {}
|
149
|
+
|
150
|
+
if "dependencies" not in project_data:
|
151
|
+
return dependencies
|
152
|
+
|
153
|
+
deps_list = project_data["dependencies"]
|
154
|
+
if not isinstance(deps_list, list):
|
155
|
+
console.warning("dependencies should be a list in pyproject.toml")
|
156
|
+
return dependencies
|
157
|
+
|
158
|
+
for dep in deps_list:
|
159
|
+
if not isinstance(dep, str):
|
160
|
+
console.warning(f"Skipping non-string dependency: {dep}")
|
161
|
+
continue
|
162
|
+
|
163
|
+
try:
|
164
|
+
name, version_spec = parse_dependency_string(dep)
|
165
|
+
if name: # Only add if we got a valid name
|
166
|
+
dependencies[name] = version_spec
|
167
|
+
except Exception as e:
|
168
|
+
console.warning(f"Failed to parse dependency '{dep}': {e}")
|
169
|
+
continue
|
170
|
+
|
171
|
+
return dependencies
|
172
|
+
|
173
|
+
|
174
|
+
def parse_dependency_string(dependency: str) -> Tuple[str, str]:
|
175
|
+
"""Parse a dependency string into package name and version specifier.
|
176
|
+
|
177
|
+
Handles PEP 508 dependency specifications including:
|
178
|
+
- Simple names: "requests"
|
179
|
+
- Version specifiers: "requests>=2.28.0"
|
180
|
+
- Complex specifiers: "requests>=2.28.0,<3.0.0"
|
181
|
+
- Extras: "requests[security]>=2.28.0"
|
182
|
+
- Environment markers: "requests>=2.28.0; python_version>='3.8'"
|
183
|
+
|
184
|
+
Args:
|
185
|
+
dependency: Raw dependency string from pyproject.toml
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
Tuple of (package_name, version_specifier)
|
189
|
+
|
190
|
+
Examples:
|
191
|
+
"requests" -> ("requests", "*")
|
192
|
+
"requests>=2.28.0" -> ("requests", ">=2.28.0")
|
193
|
+
"requests>=2.28.0,<3.0.0" -> ("requests", ">=2.28.0,<3.0.0")
|
194
|
+
"requests[security]>=2.28.0" -> ("requests", ">=2.28.0")
|
195
|
+
"""
|
196
|
+
# Remove whitespace
|
197
|
+
dependency = dependency.strip()
|
198
|
+
|
199
|
+
# Handle environment markers (everything after semicolon)
|
200
|
+
if ";" in dependency:
|
201
|
+
dependency = dependency.split(";")[0].strip()
|
202
|
+
|
203
|
+
# Pattern to match package name with optional extras and version specifiers
|
204
|
+
# Matches: package_name[extras] version_specs
|
205
|
+
pattern = r"^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)(\[[^\]]+\])?(.*)"
|
206
|
+
match = re.match(pattern, dependency)
|
207
|
+
|
208
|
+
if not match:
|
209
|
+
# Fallback for edge cases
|
210
|
+
return dependency, "*"
|
211
|
+
|
212
|
+
package_name = match.group(1)
|
213
|
+
version_part = match.group(4).strip() if match.group(4) else ""
|
214
|
+
|
215
|
+
# If no version specifier, return wildcard
|
216
|
+
if not version_part:
|
217
|
+
return package_name, "*"
|
218
|
+
|
219
|
+
# Clean up version specifier
|
220
|
+
version_spec = version_part.strip()
|
221
|
+
|
222
|
+
# Validate that version specifier starts with a valid operator
|
223
|
+
valid_operators = [">=", "<=", "==", "!=", "~=", ">", "<"]
|
224
|
+
if not any(version_spec.startswith(op) for op in valid_operators):
|
225
|
+
# If it doesn't start with an operator, treat as exact version
|
226
|
+
if version_spec:
|
227
|
+
version_spec = f"=={version_spec}"
|
228
|
+
else:
|
229
|
+
version_spec = "*"
|
230
|
+
|
231
|
+
return package_name, version_spec
|
232
|
+
|
233
|
+
|
234
|
+
def read_toml_project(file_path: str) -> dict:
|
235
|
+
"""Read and parse pyproject.toml file with improved error handling and validation.
|
236
|
+
|
237
|
+
Args:
|
238
|
+
file_path: Path to pyproject.toml file
|
239
|
+
|
240
|
+
Returns:
|
241
|
+
Dictionary containing project metadata and dependencies
|
242
|
+
"""
|
243
|
+
try:
|
244
|
+
with open(file_path, "rb") as f:
|
245
|
+
content = tomllib.load(f)
|
246
|
+
except Exception as e:
|
247
|
+
console.error(f"Failed to read or parse pyproject.toml: {e}")
|
248
|
+
|
249
|
+
# Validate required sections
|
250
|
+
if "project" not in content:
|
251
|
+
console.error("pyproject.toml is missing the required field: project.")
|
252
|
+
|
253
|
+
project = content["project"]
|
254
|
+
|
255
|
+
# Validate required fields with better error messages
|
256
|
+
required_fields = {
|
257
|
+
"name": "Project name is required in pyproject.toml",
|
258
|
+
"description": "Project description is required in pyproject.toml",
|
259
|
+
"version": "Project version is required in pyproject.toml",
|
260
|
+
}
|
261
|
+
|
262
|
+
for field, error_msg in required_fields.items():
|
263
|
+
if field not in project:
|
264
|
+
console.error(
|
265
|
+
f"pyproject.toml is missing the required field: project.{field}. {error_msg}"
|
266
|
+
)
|
267
|
+
|
268
|
+
# Check for empty values only if field exists
|
269
|
+
if field in project and (
|
270
|
+
not project[field]
|
271
|
+
or (isinstance(project[field], str) and not project[field].strip())
|
272
|
+
):
|
273
|
+
console.error(
|
274
|
+
f"Project {field} cannot be empty. Please specify a {field} in pyproject.toml."
|
275
|
+
)
|
276
|
+
|
277
|
+
# Extract author information safely
|
278
|
+
authors = project.get("authors", [])
|
279
|
+
author_name = ""
|
280
|
+
|
281
|
+
if authors and isinstance(authors, list) and len(authors) > 0:
|
282
|
+
first_author = authors[0]
|
283
|
+
if isinstance(first_author, dict):
|
284
|
+
author_name = first_author.get("name", "")
|
285
|
+
elif isinstance(first_author, str):
|
286
|
+
# Handle case where authors is a list of strings
|
287
|
+
author_name = first_author
|
288
|
+
|
289
|
+
# Extract dependencies with improved parsing
|
290
|
+
dependencies = extract_dependencies_from_toml(project)
|
291
|
+
|
292
|
+
return {
|
293
|
+
"name": project["name"].strip(),
|
294
|
+
"description": project["description"].strip(),
|
295
|
+
"version": project["version"].strip(),
|
296
|
+
"authors": author_name.strip(),
|
297
|
+
"dependencies": dependencies,
|
298
|
+
}
|
299
|
+
|
300
|
+
|
301
|
+
def files_to_include(
|
302
|
+
config_data: Optional[dict[Any, Any]], directory: str
|
303
|
+
) -> list[FileInfo]:
|
304
|
+
"""Get list of files to include in the project based on configuration.
|
305
|
+
|
306
|
+
Walks through the directory tree and identifies files to include based on extensions
|
307
|
+
and explicit inclusion rules. Skips virtual environments and hidden directories.
|
308
|
+
|
309
|
+
Args:
|
310
|
+
settings_section: Configuration section containing file inclusion rules
|
311
|
+
directory: Root directory to search for files
|
312
|
+
|
313
|
+
Returns:
|
314
|
+
list[FileInfo]: List of file information objects for included files
|
315
|
+
"""
|
316
|
+
file_extensions_included = [".py", ".mermaid", ".json", ".yaml", ".yml", ".md"]
|
317
|
+
files_included = ["pyproject.toml"]
|
318
|
+
if "settings" in config_data:
|
319
|
+
settings = config_data["settings"]
|
320
|
+
if "fileExtensionsIncluded" in settings:
|
321
|
+
file_extensions_included.extend(settings["fileExtensionsIncluded"])
|
322
|
+
if "filesIncluded" in settings:
|
323
|
+
files_included.extend(settings["filesIncluded"])
|
324
|
+
|
325
|
+
def is_venv_dir(d: str) -> bool:
|
326
|
+
"""Check if a directory is a Python virtual environment.
|
327
|
+
|
328
|
+
Args:
|
329
|
+
d: Directory path to check
|
330
|
+
|
331
|
+
Returns:
|
332
|
+
bool: True if directory is a virtual environment, False otherwise
|
333
|
+
"""
|
334
|
+
return (
|
335
|
+
os.path.exists(os.path.join(d, "Scripts", "activate"))
|
336
|
+
if os.name == "nt"
|
337
|
+
else os.path.exists(os.path.join(d, "bin", "activate"))
|
338
|
+
)
|
339
|
+
|
340
|
+
extra_files: list[FileInfo] = []
|
341
|
+
# Walk through directory and return all files in the allowlist
|
342
|
+
for root, dirs, files in os.walk(directory):
|
343
|
+
# Skip all directories that start with . or are a venv
|
344
|
+
dirs[:] = [
|
345
|
+
d
|
346
|
+
for d in dirs
|
347
|
+
if not d.startswith(".") and not is_venv_dir(os.path.join(root, d))
|
348
|
+
]
|
349
|
+
for file in files:
|
350
|
+
file_extension = os.path.splitext(file)[1].lower()
|
351
|
+
if file_extension in file_extensions_included or file in files_included:
|
352
|
+
file_path = os.path.join(root, file)
|
353
|
+
rel_path = os.path.relpath(file_path, directory)
|
354
|
+
extra_files.append(
|
355
|
+
FileInfo(
|
356
|
+
file_path=file_path,
|
357
|
+
relative_path=rel_path,
|
358
|
+
is_binary=is_binary_file(file_extension),
|
359
|
+
)
|
360
|
+
)
|
361
|
+
return extra_files
|
@@ -0,0 +1,138 @@
|
|
1
|
+
from typing import List, Optional, Union
|
2
|
+
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
4
|
+
|
5
|
+
|
6
|
+
class ProjectFile(BaseModel):
|
7
|
+
"""Model representing a file in a UiPath project.
|
8
|
+
|
9
|
+
Attributes:
|
10
|
+
id: The unique identifier of the file
|
11
|
+
name: The name of the file
|
12
|
+
is_main: Whether this is a main file
|
13
|
+
file_type: The type of the file
|
14
|
+
is_entry_point: Whether this is an entry point
|
15
|
+
ignored_from_publish: Whether this file is ignored during publish
|
16
|
+
app_form_id: The ID of the associated app form
|
17
|
+
external_automation_id: The ID of the external automation
|
18
|
+
test_case_id: The ID of the associated test case
|
19
|
+
"""
|
20
|
+
|
21
|
+
model_config = ConfigDict(
|
22
|
+
validate_by_name=True,
|
23
|
+
validate_by_alias=True,
|
24
|
+
use_enum_values=True,
|
25
|
+
arbitrary_types_allowed=True,
|
26
|
+
extra="allow",
|
27
|
+
)
|
28
|
+
|
29
|
+
id: str = Field(alias="id")
|
30
|
+
name: str = Field(alias="name")
|
31
|
+
is_main: Optional[bool] = Field(default=None, alias="isMain")
|
32
|
+
file_type: Optional[str] = Field(default=None, alias="fileType")
|
33
|
+
is_entry_point: Optional[bool] = Field(default=None, alias="isEntryPoint")
|
34
|
+
ignored_from_publish: Optional[bool] = Field(
|
35
|
+
default=None, alias="ignoredFromPublish"
|
36
|
+
)
|
37
|
+
app_form_id: Optional[str] = Field(default=None, alias="appFormId")
|
38
|
+
external_automation_id: Optional[str] = Field(
|
39
|
+
default=None, alias="externalAutomationId"
|
40
|
+
)
|
41
|
+
test_case_id: Optional[str] = Field(default=None, alias="testCaseId")
|
42
|
+
|
43
|
+
@field_validator("file_type", mode="before")
|
44
|
+
@classmethod
|
45
|
+
def convert_file_type(cls, v: Union[str, int, None]) -> Optional[str]:
|
46
|
+
"""Convert numeric file type to string.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
v: The value to convert
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
Optional[str]: The converted value or None
|
53
|
+
"""
|
54
|
+
if isinstance(v, int):
|
55
|
+
return str(v)
|
56
|
+
return v
|
57
|
+
|
58
|
+
|
59
|
+
class ProjectFolder(BaseModel):
|
60
|
+
"""Model representing a folder in a UiPath project structure.
|
61
|
+
|
62
|
+
Attributes:
|
63
|
+
id: The unique identifier of the folder
|
64
|
+
name: The name of the folder
|
65
|
+
folders: List of subfolders
|
66
|
+
files: List of files in the folder
|
67
|
+
folder_type: The type of the folder
|
68
|
+
"""
|
69
|
+
|
70
|
+
model_config = ConfigDict(
|
71
|
+
validate_by_name=True,
|
72
|
+
validate_by_alias=True,
|
73
|
+
use_enum_values=True,
|
74
|
+
arbitrary_types_allowed=True,
|
75
|
+
extra="allow",
|
76
|
+
)
|
77
|
+
|
78
|
+
id: str = Field(alias="id")
|
79
|
+
name: str = Field(alias="name")
|
80
|
+
folders: List["ProjectFolder"] = Field(default_factory=list)
|
81
|
+
files: List[ProjectFile] = Field(default_factory=list)
|
82
|
+
folder_type: Optional[str] = Field(default=None, alias="folderType")
|
83
|
+
|
84
|
+
@field_validator("folder_type", mode="before")
|
85
|
+
@classmethod
|
86
|
+
def convert_folder_type(cls, v: Union[str, int, None]) -> Optional[str]:
|
87
|
+
"""Convert numeric folder type to string.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
v: The value to convert
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
Optional[str]: The converted value or None
|
94
|
+
"""
|
95
|
+
if isinstance(v, int):
|
96
|
+
return str(v)
|
97
|
+
return v
|
98
|
+
|
99
|
+
|
100
|
+
class ProjectStructure(BaseModel):
|
101
|
+
"""Model representing the complete file structure of a UiPath project.
|
102
|
+
|
103
|
+
Attributes:
|
104
|
+
id: The unique identifier of the root folder (optional)
|
105
|
+
name: The name of the root folder (optional)
|
106
|
+
folders: List of folders in the project
|
107
|
+
files: List of files at the root level
|
108
|
+
folder_type: The type of the root folder (optional)
|
109
|
+
"""
|
110
|
+
|
111
|
+
model_config = ConfigDict(
|
112
|
+
validate_by_name=True,
|
113
|
+
validate_by_alias=True,
|
114
|
+
use_enum_values=True,
|
115
|
+
arbitrary_types_allowed=True,
|
116
|
+
extra="allow",
|
117
|
+
)
|
118
|
+
|
119
|
+
id: Optional[str] = Field(default=None, alias="id")
|
120
|
+
name: Optional[str] = Field(default=None, alias="name")
|
121
|
+
folders: List[ProjectFolder] = Field(default_factory=list)
|
122
|
+
files: List[ProjectFile] = Field(default_factory=list)
|
123
|
+
folder_type: Optional[str] = Field(default=None, alias="folderType")
|
124
|
+
|
125
|
+
@field_validator("folder_type", mode="before")
|
126
|
+
@classmethod
|
127
|
+
def convert_folder_type(cls, v: Union[str, int, None]) -> Optional[str]:
|
128
|
+
"""Convert numeric folder type to string.
|
129
|
+
|
130
|
+
Args:
|
131
|
+
v: The value to convert
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
Optional[str]: The converted value or None
|
135
|
+
"""
|
136
|
+
if isinstance(v, int):
|
137
|
+
return str(v)
|
138
|
+
return v
|
@@ -0,0 +1,114 @@
|
|
1
|
+
import os
|
2
|
+
import subprocess
|
3
|
+
|
4
|
+
from .._utils._console import ConsoleLogger
|
5
|
+
|
6
|
+
console = ConsoleLogger()
|
7
|
+
|
8
|
+
|
9
|
+
def handle_uv_operations(directory: str) -> None:
|
10
|
+
"""Handle UV package manager operations for the project.
|
11
|
+
|
12
|
+
This function checks if UV is available and if the project uses UV,
|
13
|
+
then ensures the lock file is up to date by running 'uv lock'.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
directory: The project root directory where UV operations should be performed
|
17
|
+
|
18
|
+
Note:
|
19
|
+
This function will silently return if UV is not available or if the project
|
20
|
+
doesn't use UV (no uv.lock file present).
|
21
|
+
"""
|
22
|
+
if not is_uv_available():
|
23
|
+
return
|
24
|
+
if not is_uv_project(directory):
|
25
|
+
return
|
26
|
+
# Always run uv lock to ensure lock file is up to date
|
27
|
+
run_uv_lock(directory)
|
28
|
+
|
29
|
+
|
30
|
+
def is_uv_available() -> bool:
|
31
|
+
"""Check if UV package manager is available in the system.
|
32
|
+
|
33
|
+
Attempts to run 'uv --version' to verify UV is installed and accessible.
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
bool: True if UV is available and working, False otherwise
|
37
|
+
|
38
|
+
Note:
|
39
|
+
This function will return False if:
|
40
|
+
- UV is not installed
|
41
|
+
- UV command fails to execute
|
42
|
+
- Any unexpected error occurs during version check
|
43
|
+
"""
|
44
|
+
try:
|
45
|
+
subprocess.run(["uv", "--version"], capture_output=True, check=True, timeout=20)
|
46
|
+
return True
|
47
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
48
|
+
return False
|
49
|
+
except Exception as e:
|
50
|
+
console.warning(
|
51
|
+
f"An unexpected error occurred while checking uv availability: {str(e)}"
|
52
|
+
)
|
53
|
+
return False
|
54
|
+
|
55
|
+
|
56
|
+
def is_uv_project(directory: str) -> bool:
|
57
|
+
"""Check if the project uses UV package manager.
|
58
|
+
|
59
|
+
Determines if this is a UV project by checking for the presence
|
60
|
+
of a uv.lock file in the project directory.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
directory: The project root directory to check
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
bool: True if uv.lock exists, indicating a UV project, False otherwise
|
67
|
+
"""
|
68
|
+
uv_lock_path = os.path.join(directory, "uv.lock")
|
69
|
+
|
70
|
+
# If uv.lock exists, it's definitely a uv project
|
71
|
+
if os.path.exists(uv_lock_path):
|
72
|
+
return True
|
73
|
+
|
74
|
+
return False
|
75
|
+
|
76
|
+
|
77
|
+
def run_uv_lock(directory: str) -> bool:
|
78
|
+
"""Run 'uv lock' command to update the project's lock file.
|
79
|
+
|
80
|
+
Executes UV lock command to ensure dependencies are properly locked
|
81
|
+
and the lock file is up to date.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
directory: The project root directory where the lock command should be run
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
bool: True if the lock command succeeds, False if it fails for any reason
|
88
|
+
|
89
|
+
Note:
|
90
|
+
This function will log warnings and return False if:
|
91
|
+
- The UV command fails to execute
|
92
|
+
- UV is not found in the system
|
93
|
+
- The lock command times out (60 seconds)
|
94
|
+
- Any unexpected error occurs during execution
|
95
|
+
"""
|
96
|
+
try:
|
97
|
+
subprocess.run(
|
98
|
+
["uv", "lock"],
|
99
|
+
cwd=directory,
|
100
|
+
capture_output=True,
|
101
|
+
text=True,
|
102
|
+
check=True,
|
103
|
+
timeout=60,
|
104
|
+
)
|
105
|
+
return True
|
106
|
+
except subprocess.CalledProcessError as e:
|
107
|
+
console.warning(f"uv lock failed: {e.stderr}")
|
108
|
+
return False
|
109
|
+
except FileNotFoundError:
|
110
|
+
console.warning("uv command not found. Skipping lock file update.")
|
111
|
+
return False
|
112
|
+
except Exception as e:
|
113
|
+
console.warning(f"An unexpected error occurred while running uv lock: {str(e)}")
|
114
|
+
return False
|