diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 7435d721b..92d5a5a4b 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.15.1" +version = "2.15.2" description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py b/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py index 3fc264e41..92fc267f9 100644 --- a/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py +++ b/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py @@ -6,11 +6,13 @@ from datetime import datetime, timezone from typing import AsyncIterator +import anyio import click from uipath._cli.models.uipath_json_schema import PackOptions from uipath.platform.common import UiPathConfig from uipath.platform.constants import ( + ENTRY_POINTS_FILE, EVALS_FOLDER, LEGACY_EVAL_FOLDER, PYTHON_CONFIGURATION_FILE, @@ -162,15 +164,27 @@ async def _process_file_uploads( ) processed_source_files: set[str] = set() updates: list[UpdateEvent] = [] + # Written through the EntryPoints endpoint once the sources have landed, + # so Studio Web announces a contract that matches the uploaded code. + entry_points_content: str | None = None for local_file in local_files: if not os.path.exists(local_file.file_path): logger.info(f"File not found: '{local_file.file_path}'") continue - remote_file = remote_files.get( - local_file.relative_path.replace("\\", "/"), None - ) + relative_path = local_file.relative_path.replace("\\", "/") + remote_file = remote_files.get(relative_path, None) + + if relative_path == ENTRY_POINTS_FILE: + if remote_file: + processed_source_files.add(remote_file.id) + ( + entry_points_content, + entry_points_event, + ) = await self._prepare_entry_points_file(local_file, remote_file) + updates.append(entry_points_event) + continue if remote_file: processed_source_files.add(remote_file.id) @@ -288,11 +302,69 @@ async def _process_file_uploads( structural_migration ) + if entry_points_content is not None: + await self._studio_client.set_entry_points_async(entry_points_content) + # Clean up empty folders after migration await self._cleanup_empty_folders() return updates + async def _prepare_entry_points_file( + self, + local_file: FileInfo, + remote_file: ProjectFile | None, + ) -> tuple[str | None, UpdateEvent]: + """Decide whether entry-points.json must be written and describe the operation. + + The file is kept out of the structural migration: Studio Web only raises + its entry-points-changed notification for the dedicated EntryPoints + endpoint, and that notification is what lets designers consuming this + project (for example a Flow that invokes it) refresh its inputs and + outputs. A plain file write updates the file silently. + + Args: + local_file: The local entry-points.json. + remote_file: The remote entry-points.json, if the project has one. + + Returns: + The content to send through the EntryPoints endpoint (None when the + remote copy is already identical) and the progress event to report. + """ + local_content = await anyio.Path(local_file.file_path).read_text( + encoding="utf-8" + ) + + if remote_file is None: + return local_content, UpdateEvent( + file_path=local_file.file_path, + status="uploading", + message=f"Uploading '{local_file.file_name}'", + ) + + try: + remote_response = await self._studio_client.download_project_file_async( + remote_file + ) + remote_content = remote_response.read().decode("utf-8") + if compute_normalized_hash(local_content) == compute_normalized_hash( + remote_content + ): + return None, UpdateEvent( + file_path=local_file.file_path, + status="up_to_date", + message=f"File '{local_file.file_name}' is up to date", + ) + except Exception as e: + logger.warning(f"Failed to compare file '{local_file.file_path}': {e}") + # If comparison fails, proceed with update + + return local_content, UpdateEvent( + file_path=local_file.file_path, + status="updating", + message=f"Updating '{local_file.file_name}'", + ) + def _collect_deleted_files( self, remote_files: dict[str, ProjectFile], diff --git a/packages/uipath/src/uipath/_cli/_utils/_studio_project.py b/packages/uipath/src/uipath/_cli/_utils/_studio_project.py index 84ad03284..f14cc1e58 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_studio_project.py +++ b/packages/uipath/src/uipath/_cli/_utils/_studio_project.py @@ -855,6 +855,37 @@ async def upload_file_async( # response contains only the uploaded file identifier return response.json(), action + @traced(name="set_entry_points", run_type="uipath") + @with_lock_retry + async def set_entry_points_async( + self, + content: str, + headers: Optional[dict[str, Any]] = None, + ) -> str: + """Create or overwrite the project's entry-points.json. + + Studio Web raises its entry-points-changed notification only for writes + made through this endpoint, not for plain file writes. Designers that + consume this project's contract (for example a Flow that invokes it) + refresh their copy of the inputs and outputs from that notification. + + Args: + content: The JSON text of entry-points.json. + headers: HTTP headers (the lock key is injected by the decorator). + + Returns: + str: The id of the entry-points.json file in the project. + """ + request_headers = {**(headers or {}), "Content-Type": "application/json"} + response = await self.uipath.api_client.request_async( + "POST", + url=f"{self.file_operations_base_url}/EntryPoints", + scoped="org", + content=content.encode("utf-8"), + headers=request_headers, + ) + return response.text.strip().strip('"') + @traced(name="delete_file", run_type="uipath") @with_lock_retry async def delete_item_async( diff --git a/packages/uipath/tests/cli/test_push.py b/packages/uipath/tests/cli/test_push.py index 95a870701..2693aee20 100644 --- a/packages/uipath/tests/cli/test_push.py +++ b/packages/uipath/tests/cli/test_push.py @@ -126,6 +126,16 @@ def _mock_file_download( text="Remote file content" if not file_content else file_content, ) + def _mock_entry_points_upload( + self, httpx_mock: HTTPXMock, base_url: str, project_id: str + ): + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints", + status_code=200, + text='"entry-points-file-id"', + ) + def _create_required_files(self, exclude: list[str] | None = None): required_files = ["uipath.json", "bindings.json", "entry-points.json"] for file in required_files: @@ -296,7 +306,7 @@ def test_successful_push( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") self._mock_file_download(httpx_mock, "456") @@ -325,6 +335,7 @@ def test_successful_push( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) # Mock empty folder cleanup - get structure again after migration httpx_mock.add_response( @@ -419,7 +430,7 @@ def test_successful_push_new_project( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") @@ -429,6 +440,7 @@ def test_successful_push_new_project( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) # Mock empty folder cleanup - get structure again after migration httpx_mock.add_response( @@ -534,7 +546,7 @@ def test_first_push_with_minimal_studio_metadata_from_init( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -543,6 +555,7 @@ def test_first_push_with_minimal_studio_metadata_from_init( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -810,7 +823,7 @@ def test_push_to_scaffolded_function_project( json={"id": project_id, "projectType": "Function"}, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) httpx_mock.add_response( method="POST", @@ -818,6 +831,7 @@ def test_push_to_scaffolded_function_project( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -947,7 +961,7 @@ def test_push_with_nolock_flag( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") self._mock_file_download(httpx_mock, "789") self._mock_file_download(httpx_mock, "456") @@ -958,6 +972,7 @@ def test_push_with_nolock_flag( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1034,7 +1049,7 @@ def test_push_files_excluded( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1043,6 +1058,7 @@ def test_push_files_excluded( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1113,7 +1129,7 @@ def test_push_files_excluded_takes_precedence_over_included( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1122,6 +1138,7 @@ def test_push_files_excluded_takes_precedence_over_included( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1191,7 +1208,7 @@ def test_push_filename_vs_path_exclusion( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1200,6 +1217,7 @@ def test_push_filename_vs_path_exclusion( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1281,7 +1299,7 @@ def test_push_filename_vs_path_inclusion( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1290,6 +1308,7 @@ def test_push_filename_vs_path_inclusion( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1387,7 +1406,7 @@ def test_push_directory_name_vs_path_exclusion( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) httpx_mock.add_response( method="POST", @@ -1395,6 +1414,7 @@ def test_push_directory_name_vs_path_exclusion( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1507,7 +1527,7 @@ def test_push_detects_source_file_conflicts( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") self._mock_file_download(httpx_mock, "456") @@ -1518,6 +1538,7 @@ def test_push_detects_source_file_conflicts( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) with runner.isolated_filesystem(temp_dir=temp_dir): self._create_required_files() @@ -1594,7 +1615,7 @@ def test_push_shows_up_to_date_for_unchanged_files( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") # Mock file downloads - return same content as local files @@ -1607,6 +1628,7 @@ def test_push_shows_up_to_date_for_unchanged_files( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) with runner.isolated_filesystem(temp_dir=temp_dir): self._create_required_files() @@ -1664,7 +1686,7 @@ def test_push_skips_root_evals_folder_files( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1673,6 +1695,7 @@ def test_push_skips_root_evals_folder_files( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1758,7 +1781,7 @@ def test_push_skips_only_root_evals_files_not_subdirectories( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1767,6 +1790,7 @@ def test_push_skips_only_root_evals_files_not_subdirectories( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1844,7 +1868,7 @@ def test_push_logs_skipped_files_count( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1853,6 +1877,7 @@ def test_push_logs_skipped_files_count( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1973,7 +1998,7 @@ def test_push_preserves_remote_evals_when_no_local_evals( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "main-456") self._mock_file_download(httpx_mock, "123") @@ -1984,6 +2009,7 @@ def test_push_preserves_remote_evals_when_no_local_evals( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) with runner.isolated_filesystem(temp_dir=temp_dir): self._create_required_files() @@ -2022,6 +2048,180 @@ def test_push_preserves_remote_evals_when_no_local_evals( assert b"evaluator-file-1" not in content assert b"eval-set-file-1" not in content + def _mock_structure_with_entry_points( + self, httpx_mock: HTTPXMock, base_url: str, project_id: str + ) -> None: + mock_structure = { + "id": "root", + "name": "root", + "folders": [], + "files": [ + { + "id": "123", + "name": "main.py", + "isMain": True, + "fileType": "1", + "isEntryPoint": True, + "ignoredFromPublish": False, + }, + { + "id": "456", + "name": "pyproject.toml", + "isMain": False, + "fileType": "1", + "isEntryPoint": False, + "ignoredFromPublish": False, + }, + { + "id": "898", + "name": "entry-points.json", + "isMain": False, + "fileType": "1", + "isEntryPoint": False, + "ignoredFromPublish": False, + }, + ], + "folderType": "0", + } + # Fetched before the upload and again by the empty-folder cleanup. + for _ in range(2): + httpx_mock.add_response( + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", + json=mock_structure, + ) + + def test_push_writes_entry_points_through_entry_points_endpoint( + self, + runner: CliRunner, + temp_dir: str, + project_details: ProjectDetails, + mock_env_vars: dict[str, str], + httpx_mock: HTTPXMock, + ) -> None: + """entry-points.json goes through the EntryPoints endpoint, after the structural migration.""" + base_url = "https://cloud.uipath.com/organization" + project_id = "test-project-id" + local_entry_points = { + "$schema": "https://cloud.uipath.com/draft/2024-12/entry-point", + "entryPoints": [ + { + "filePath": "main", + "uniqueId": "c2a0b6a2-5e0e-4a4a-9a7a-2c5a3f6a1b10", + "type": "agent", + "input": { + "type": "object", + "properties": {"topic": {"type": "string"}}, + }, + "output": {"type": "object", "properties": {}}, + } + ], + } + + self._mock_structure_with_entry_points(httpx_mock, base_url, project_id) + # One lock lookup for the structural migration, one for the entry-points write. + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) + self._mock_file_download(httpx_mock, "123") + self._mock_file_download(httpx_mock, "456") + self._mock_file_download(httpx_mock, "898", file_content='{"entryPoints": []}') + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration", + status_code=200, + json={"success": True}, + ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) + + with runner.isolated_filesystem(temp_dir=temp_dir): + self._create_required_files(exclude=["entry-points.json"]) + with open("entry-points.json", "w") as f: + json.dump(local_entry_points, f) + with open("pyproject.toml", "w") as f: + f.write(project_details.to_toml()) + with open("main.py", "w") as f: + f.write("print('Hello World')") + + configure_env_vars(mock_env_vars) + os.environ["UIPATH_PROJECT_ID"] = project_id + + result = runner.invoke(cli, ["push", "./", "--ignore-resources"]) + assert result.exit_code == 0, result.output + assert "Updating 'entry-points.json'" in result.output + + entry_points_url = f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints" + migration_url = f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration" + + entry_points_request = httpx_mock.get_request( + method="POST", url=entry_points_url + ) + assert entry_points_request is not None + assert json.loads(entry_points_request.content) == local_entry_points + assert entry_points_request.headers["Content-Type"] == "application/json" + assert ( + entry_points_request.headers["x-uipath-sw-lockkey"] == "test-lock-key" + ) + + migration_request = httpx_mock.get_request(method="POST", url=migration_url) + assert migration_request is not None + assert b"entry-points.json" not in migration_request.content + assert ( + b"ModifiedResources[0].Id\r\n\r\n898" not in migration_request.content + ) + + ordered_urls = [str(request.url) for request in httpx_mock.get_requests()] + assert ordered_urls.index(migration_url) < ordered_urls.index( + entry_points_url + ) + + def test_push_skips_entry_points_endpoint_when_unchanged( + self, + runner: CliRunner, + temp_dir: str, + project_details: ProjectDetails, + mock_env_vars: dict[str, str], + httpx_mock: HTTPXMock, + ) -> None: + """An unchanged entry-points.json is neither migrated nor re-announced.""" + base_url = "https://cloud.uipath.com/organization" + project_id = "test-project-id" + local_entry_points = {"entryPoints": [{"filePath": "main", "type": "agent"}]} + + self._mock_structure_with_entry_points(httpx_mock, base_url, project_id) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_file_download(httpx_mock, "123") + self._mock_file_download(httpx_mock, "456") + self._mock_file_download( + httpx_mock, "898", file_content=json.dumps(local_entry_points, indent=4) + ) + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration", + status_code=200, + json={"success": True}, + ) + + with runner.isolated_filesystem(temp_dir=temp_dir): + self._create_required_files(exclude=["entry-points.json"]) + with open("entry-points.json", "w") as f: + json.dump(local_entry_points, f) + with open("pyproject.toml", "w") as f: + f.write(project_details.to_toml()) + with open("main.py", "w") as f: + f.write("print('Hello World')") + + configure_env_vars(mock_env_vars) + os.environ["UIPATH_PROJECT_ID"] = project_id + + result = runner.invoke(cli, ["push", "./", "--ignore-resources"]) + assert result.exit_code == 0, result.output + assert "File 'entry-points.json' is up to date" in result.output + assert ( + httpx_mock.get_request( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints", + ) + is None + ) + class TestResourceCreation: """Test resource creation and import functionality during push.""" @@ -2099,6 +2299,7 @@ def test_push_with_resources_imports_referenced_resources( "projectLockKey": "test-lock-key", "solutionLockKey": "test-solution-lock-key", }, + is_reusable=True, ) httpx_mock.add_response( @@ -2107,6 +2308,12 @@ def test_push_with_resources_imports_referenced_resources( status_code=200, json={"success": True}, ) + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints", + status_code=200, + text='"entry-points-file-id"', + ) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -2293,6 +2500,7 @@ def test_push_with_ignore_resources_flag_skips_resource_import( "projectLockKey": "test-lock-key", "solutionLockKey": "test-solution-lock-key", }, + is_reusable=True, ) httpx_mock.add_response( @@ -2301,6 +2509,12 @@ def test_push_with_ignore_resources_flag_skips_resource_import( status_code=200, json={"success": True}, ) + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints", + status_code=200, + text='"entry-points-file-id"', + ) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -2407,6 +2621,7 @@ def test_push_with_resource_not_found_creates_virtual( "projectLockKey": "test-lock-key", "solutionLockKey": "test-solution-lock-key", }, + is_reusable=True, ) httpx_mock.add_response( @@ -2415,6 +2630,12 @@ def test_push_with_resource_not_found_creates_virtual( status_code=200, json={"success": True}, ) + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints", + status_code=200, + text='"entry-points-file-id"', + ) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -2576,6 +2797,7 @@ def test_push_with_resource_already_exists_shows_unchanged( "projectLockKey": "test-lock-key", "solutionLockKey": "test-solution-lock-key", }, + is_reusable=True, ) self._mock_file_download(httpx_mock, "123") @@ -2585,6 +2807,12 @@ def test_push_with_resource_already_exists_shows_unchanged( status_code=200, json={"success": True}, ) + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints", + status_code=200, + text='"entry-points-file-id"', + ) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 7cdb06e88..4d69c7a08 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2599,7 +2599,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.15.1" +version = "2.15.2" source = { editable = "." } dependencies = [ { name = "applicationinsights" },