From 4b7e127eb552ab8afd6cea81c985f9aa3a92bad4 Mon Sep 17 00:00:00 2001 From: Cristian Pufu Date: Sun, 30 Nov 2025 18:47:13 +0200 Subject: [PATCH] fix: remove warnings, upgrade typing --- docs/core/getting_started.md | 5 +- docs/eval/custom_evaluators.md | 22 +- pyproject.toml | 6 +- samples/asset-modifier-agent/main.py | 7 +- .../multi_tool_agent/agent.py | 5 +- samples/llm_chat_agent/agent.py | 10 +- scripts/lint_httpx_client.py | 474 +++++++++--------- scripts/update_agents_md.py | 6 +- src/uipath/_cli/_auth/_auth_server.py | 5 +- src/uipath/_cli/_auth/_auth_service.py | 13 +- src/uipath/_cli/_auth/_oidc_utils.py | 5 +- src/uipath/_cli/_auth/_portal_service.py | 17 +- src/uipath/_cli/_auth/_url_utils.py | 6 +- src/uipath/_cli/_auth/_utils.py | 3 +- src/uipath/_cli/_debug/_bridge.py | 2 +- .../_cli/_evals/_console_progress_reporter.py | 6 +- src/uipath/_cli/_evals/_evaluator_factory.py | 34 +- src/uipath/_cli/_evals/_helpers.py | 8 +- .../_cli/_evals/_models/_evaluation_set.py | 58 +-- src/uipath/_cli/_evals/_models/_output.py | 16 +- src/uipath/_cli/_evals/_progress_reporter.py | 20 +- src/uipath/_cli/_evals/_runtime.py | 22 +- src/uipath/_cli/_evals/_span_collection.py | 7 +- src/uipath/_cli/_evals/mocks/cache_manager.py | 18 +- src/uipath/_cli/_evals/mocks/input_mocker.py | 6 +- src/uipath/_cli/_evals/mocks/mocks.py | 15 +- src/uipath/_cli/_push/sw_file_handler.py | 18 +- src/uipath/_cli/_templates/main.py.template | 5 +- src/uipath/_cli/_utils/_common.py | 8 +- src/uipath/_cli/_utils/_console.py | 22 +- src/uipath/_cli/_utils/_eval_set.py | 3 +- src/uipath/_cli/_utils/_input_args.py | 172 ------- src/uipath/_config.py | 7 +- src/uipath/_execution_context.py | 13 +- src/uipath/_folder_context.py | 6 +- src/uipath/_services/guardrails_service.py | 8 +- src/uipath/_services/llm_gateway_service.py | 31 +- src/uipath/_services/mcp_service.py | 16 +- src/uipath/functions/schema_gen.py | 7 +- src/uipath/utils/_endpoints_manager.py | 5 +- src/uipath/utils/dynamic_schema.py | 10 +- testcases/basic-testcase/main.py | 6 +- tests/cli/contract/test_sdk_cli_alignment.py | 12 +- tests/cli/mocks/simple_script.py | 5 +- tests/cli/test_input_args.py | 24 +- tests/cli/test_pull.py | 12 +- tests/cli/test_push.py | 48 +- tests/cli/test_run.py | 5 +- tests/cli/utils/common.py | 3 +- tests/cli/utils/test_dynamic_schema.py | 23 +- .../sdk/services/test_attachments_service.py | 14 +- tests/sdk/services/test_entities_service.py | 4 +- tests/sdk/services/test_llm_schema_cleanup.py | 20 +- tests/sdk/services/test_llm_service.py | 13 +- uv.lock | 18 +- 55 files changed, 570 insertions(+), 764 deletions(-) delete mode 100644 src/uipath/_cli/_utils/_input_args.py diff --git a/docs/core/getting_started.md b/docs/core/getting_started.md index 9720049d3..6c00b0089 100644 --- a/docs/core/getting_started.md +++ b/docs/core/getting_started.md @@ -117,14 +117,13 @@ Upon successful authentication, your project will contain a `.env` file with you Open `main.py` in your code editor. You can start with this example code: ```python from dataclasses import dataclass -from typing import Optional @dataclass class EchoIn: message: str - repeat: Optional[int] = 1 - prefix: Optional[str] = None + repeat: int | None = 1 + prefix: str | None = None @dataclass diff --git a/docs/eval/custom_evaluators.md b/docs/eval/custom_evaluators.md index 7e613f540..6e9abca4c 100644 --- a/docs/eval/custom_evaluators.md +++ b/docs/eval/custom_evaluators.md @@ -92,7 +92,6 @@ class MyEvaluatorConfig(BaseEvaluatorConfig[MyEvaluationCriteria]): Implement the core evaluation logic: ```python -from typing import List from uipath.eval.evaluators import BaseEvaluator from uipath.eval.models import AgentExecution, NumericEvaluationResult import json @@ -150,13 +149,13 @@ class MyCustomEvaluator( }), ) - def _extract_values(self, agent_execution: AgentExecution) -> List[str]: + def _extract_values(self, agent_execution: AgentExecution) -> list[str]: """Extract values from agent execution (implement your logic).""" # Your custom extraction logic here return [] def _compute_similarity( - self, actual: List[str], expected: List[str] + self, actual: list[str], expected: list[str] ) -> float: """Compute similarity score (implement your logic).""" # Your custom scoring logic here @@ -248,7 +247,7 @@ Custom evaluators often need to extract information from tool calls in the agent ```python from uipath.eval._helpers.evaluators_helpers import extract_tool_calls -def _process_tool_calls(self, agent_execution: AgentExecution) -> List[str]: +def _process_tool_calls(self, agent_execution: AgentExecution) -> list[str]: """Extract and process tool calls from the execution trace.""" tool_calls = extract_tool_calls(agent_execution.agent_trace) @@ -286,7 +285,6 @@ Here's a complete example based on real-world usage that compares data patterns ```python """Custom evaluator for pattern comparison.""" import json -from typing import List from pydantic import Field from uipath.eval.evaluators import BaseEvaluator @@ -299,7 +297,7 @@ from uipath.eval.models import AgentExecution from uipath.eval._helpers.evaluators_helpers import extract_tool_calls -def _compute_jaccard_similarity(expected: List[str], actual: List[str]) -> float: +def _compute_jaccard_similarity(expected: list[str], actual: list[str]) -> float: """Compute Jaccard similarity (intersection over union). Returns 1.0 when both expected and actual are empty (perfect match). @@ -319,7 +317,7 @@ def _compute_jaccard_similarity(expected: List[str], actual: List[str]) -> float class PatternEvaluatorCriteria(BaseEvaluationCriteria): """Evaluation criteria for pattern evaluator.""" - expected_output: List[str] = Field(default_factory=list) + expected_output: list[str] = Field(default_factory=list) class PatternEvaluatorConfig(BaseEvaluatorConfig[PatternEvaluatorCriteria]): @@ -371,7 +369,7 @@ class PatternComparisonEvaluator( }), ) - def _extract_patterns(self, agent_execution: AgentExecution) -> List[str]: + def _extract_patterns(self, agent_execution: AgentExecution) -> list[str]: """Extract patterns from tool calls. Args: @@ -418,7 +416,7 @@ def _extract_data( self, agent_execution: AgentExecution, tool_name: str -) -> List[str]: +) -> list[str]: """Extract data from specific tool calls. Args: @@ -484,8 +482,8 @@ Make your scoring logic explicit and well-documented, using config values approp ```python def _compute_score( self, - actual: List[str], - expected: List[str] + actual: list[str], + expected: list[str] ) -> float: """Compute evaluation score. @@ -618,7 +616,7 @@ def _extract_from_specific_tool( ```python def _compute_set_similarity( - self, actual: List[str], expected: List[str] + self, actual: list[str], expected: list[str] ) -> float: """Compute similarity using set operations.""" actual_set = set(actual) diff --git a/pyproject.toml b/pyproject.toml index 972d241ea..faa1389c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "uipath" -version = "2.2.7" +version = "2.2.8" 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" dependencies = [ "uipath-core>=0.0.5, <0.1.0", - "uipath-runtime>=0.1.0, <0.2.0", + "uipath-runtime>=0.1.1, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", @@ -17,7 +17,7 @@ dependencies = [ "truststore>=0.10.1", "mockito>=1.5.4", "hydra-core>=1.3.2", - "pydantic-function-models>=0.1.10", + "pydantic-function-models>=0.1.11", "pysignalr==1.3.0", "coverage>=7.8.2", "mermaid-builder==0.0.3", diff --git a/samples/asset-modifier-agent/main.py b/samples/asset-modifier-agent/main.py index 7710e393e..f41965dd2 100644 --- a/samples/asset-modifier-agent/main.py +++ b/samples/asset-modifier-agent/main.py @@ -2,8 +2,7 @@ import dotenv import logging import os - -from typing import Optional +from typing import Any from uipath.platform import UiPath from uipath.tracing import traced @@ -38,7 +37,7 @@ class AgentInput: asset_name: str folder_path: str -def get_asset(client: UiPath, name: str, folder_path: str) -> Optional[object]: +def get_asset(client: UiPath, name: str, folder_path: str) -> Any: """Retrieve an asset from UiPath. Args: @@ -46,7 +45,7 @@ def get_asset(client: UiPath, name: str, folder_path: str) -> Optional[object]: folder_path (str): The UiPath folder path. Returns: - Optional[object]: The asset object if found, else None. + Any: The asset object if found, else None. """ return client.assets.retrieve(name=name, folder_path=folder_path) diff --git a/samples/google-ADK-agent/multi_tool_agent/agent.py b/samples/google-ADK-agent/multi_tool_agent/agent.py index 7038cbdbf..bcebde62e 100644 --- a/samples/google-ADK-agent/multi_tool_agent/agent.py +++ b/samples/google-ADK-agent/multi_tool_agent/agent.py @@ -1,6 +1,5 @@ import dataclasses import datetime -from typing import Dict from zoneinfo import ZoneInfo from google.adk.agents import Agent @@ -9,7 +8,7 @@ from google.genai.types import Content, Part -def get_weather(city: str) -> Dict[str, str]: +def get_weather(city: str) -> dict[str, str]: """Retrieves the current weather report for a specified city. Args: @@ -33,7 +32,7 @@ def get_weather(city: str) -> Dict[str, str]: } -def get_current_time(city: str) -> Dict[str, str]: +def get_current_time(city: str) -> dict[str, str]: """Returns the current time in a specified city. Args: diff --git a/samples/llm_chat_agent/agent.py b/samples/llm_chat_agent/agent.py index d4ed4a791..e2f0faf43 100644 --- a/samples/llm_chat_agent/agent.py +++ b/samples/llm_chat_agent/agent.py @@ -15,7 +15,7 @@ import asyncio import json import os -from typing import Any, Dict, List +from typing import Any import httpx from dotenv import load_dotenv @@ -54,14 +54,14 @@ class AgentOutput(BaseModel): """Output model for the agent.""" response: str = Field(description="Agent's response to the user") - tool_calls_made: List[str] = Field( + tool_calls_made: list[str] = Field( default_factory=list, description="Names of tools that were called" ) # Tool implementations @traced() -async def get_current_weather(city: str) -> Dict[str, Any]: +async def get_current_weather(city: str) -> dict[str, Any]: """ Get the current weather for a city using Open-Meteo API. @@ -174,7 +174,7 @@ def get_weather_description(code: int) -> str: @traced() -async def calculate(expression: str) -> Dict[str, Any]: +async def calculate(expression: str) -> dict[str, Any]: """ Perform a mathematical calculation using Newton API. @@ -274,7 +274,7 @@ async def calculate(expression: str) -> Dict[str, Any]: @traced() -async def execute_tool_call(tool_name: str, tool_arguments: Dict[str, Any]) -> Dict[str, Any]: +async def execute_tool_call(tool_name: str, tool_arguments: dict[str, Any]) -> dict[str, Any]: """ Execute a tool based on its name and arguments. diff --git a/scripts/lint_httpx_client.py b/scripts/lint_httpx_client.py index d1562115b..c8ee79855 100644 --- a/scripts/lint_httpx_client.py +++ b/scripts/lint_httpx_client.py @@ -1,237 +1,237 @@ -#!/usr/bin/env python3 -"""Custom linter to check for httpx.Client() usage. - -This script checks for direct usage of httpx.Client() without using the -get_httpx_client_kwargs() function, which is required for proper SSL -and proxy configuration in the UiPath Python SDK. -""" - -import ast -import sys -from pathlib import Path -from typing import List, NamedTuple - - -class LintViolation(NamedTuple): - """Represents a linting violation.""" - - filename: str - line: int - column: int - message: str - rule_code: str - - -class HttpxClientChecker(ast.NodeVisitor): - """AST visitor to check for httpx.Client() usage violations.""" - - def __init__(self, filename: str): - """Initialize the checker with a filename. - - Args: - filename: The path to the file being checked. - """ - self.filename = filename - self.violations: List[LintViolation] = [] - self.has_httpx_import = False - self.has_get_httpx_client_kwargs_import = False - # Track variables that contain get_httpx_client_kwargs - self.variables_with_httpx_kwargs: set[str] = set() - - def visit_Import(self, node: ast.Import) -> None: - """Check for httpx imports.""" - for alias in node.names: - if alias.name == "httpx": - self.has_httpx_import = True - self.generic_visit(node) - - def visit_ImportFrom(self, node: ast.ImportFrom) -> None: - """Check for imports from httpx or get_httpx_client_kwargs.""" - if node.module == "httpx": - self.has_httpx_import = True - elif node.module and "get_httpx_client_kwargs" in [ - alias.name for alias in (node.names or []) - ]: - self.has_get_httpx_client_kwargs_import = True - self.generic_visit(node) - - def visit_Assign(self, node: ast.Assign) -> None: - """Track variable assignments that use get_httpx_client_kwargs.""" - if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name): - var_name = node.targets[0].id - if self._assignment_uses_get_httpx_client_kwargs(node.value): - self.variables_with_httpx_kwargs.add(var_name) - self.generic_visit(node) - - def _assignment_uses_get_httpx_client_kwargs(self, value_node: ast.AST) -> bool: - """Check if an assignment value uses get_httpx_client_kwargs.""" - if isinstance(value_node, ast.Call): - # Direct call: var = get_httpx_client_kwargs() - if isinstance(value_node.func, ast.Name): - if value_node.func.id == "get_httpx_client_kwargs": - return True - elif isinstance(value_node.func, ast.Attribute): - if value_node.func.attr == "get_httpx_client_kwargs": - return True - elif isinstance(value_node, ast.Dict): - # Dictionary that spreads get_httpx_client_kwargs: {..., **get_httpx_client_kwargs()} - for key in value_node.keys: - if key is None: # This is a **kwargs expansion - # Find corresponding value - idx = value_node.keys.index(key) - if idx < len(value_node.values): - spread_value = value_node.values[idx] - if isinstance(spread_value, ast.Call): - if isinstance(spread_value.func, ast.Name): - if spread_value.func.id == "get_httpx_client_kwargs": - return True - elif isinstance(spread_value.func, ast.Attribute): - if spread_value.func.attr == "get_httpx_client_kwargs": - return True - elif isinstance(spread_value, ast.Name): - # Spreading another variable that might contain httpx kwargs - if spread_value.id in self.variables_with_httpx_kwargs: - return True - return False - - def visit_Call(self, node: ast.Call) -> None: - """Check for httpx.Client() and httpx.AsyncClient() calls.""" - if self._is_httpx_client_call(node): - # Check if this is a proper usage with get_httpx_client_kwargs - if not self._is_using_get_httpx_client_kwargs(node): - client_type = self._get_client_type(node) - violation = LintViolation( - filename=self.filename, - line=node.lineno, - column=node.col_offset, - message=f"Use **get_httpx_client_kwargs() with {client_type}() - should be: {client_type}(**get_httpx_client_kwargs())", - rule_code="UIPATH001", - ) - self.violations.append(violation) - - self.generic_visit(node) - - def _is_httpx_client_call(self, node: ast.Call) -> bool: - """Check if the call is httpx.Client() or httpx.AsyncClient().""" - if isinstance(node.func, ast.Attribute): - if ( - isinstance(node.func.value, ast.Name) - and node.func.value.id == "httpx" - and node.func.attr in ("Client", "AsyncClient") - ): - return True - elif isinstance(node.func, ast.Name) and node.func.id in ( - "Client", - "AsyncClient", - ): - # This could be a direct Client/AsyncClient import, check if httpx is imported - return self.has_httpx_import - return False - - def _get_client_type(self, node: ast.Call) -> str: - """Get the client type name (Client or AsyncClient).""" - if isinstance(node.func, ast.Attribute): - return f"httpx.{node.func.attr}" - elif isinstance(node.func, ast.Name): - return node.func.id - return "httpx.Client" - - def _is_using_get_httpx_client_kwargs(self, node: ast.Call) -> bool: - """Check if the httpx.Client() call is using **get_httpx_client_kwargs().""" - # Check if there are any **kwargs that use get_httpx_client_kwargs directly - for keyword in node.keywords: - if keyword.arg is None: # This is a **kwargs expansion - if isinstance(keyword.value, ast.Call): - if isinstance(keyword.value.func, ast.Name): - if keyword.value.func.id == "get_httpx_client_kwargs": - return True - elif isinstance(keyword.value.func, ast.Attribute): - if keyword.value.func.attr == "get_httpx_client_kwargs": - return True - elif isinstance(keyword.value, ast.Name): - # Check if this variable might contain get_httpx_client_kwargs - # This handles cases like: **client_kwargs where client_kwargs was built from get_httpx_client_kwargs - var_name = keyword.value.id - if self._variable_contains_get_httpx_client_kwargs(var_name): - return True - - # Also check if it's the ONLY argument and it's **get_httpx_client_kwargs() - # This handles cases like: httpx.Client(**get_httpx_client_kwargs()) - if len(node.args) == 0 and len(node.keywords) == 1: - keyword = node.keywords[0] - if keyword.arg is None and isinstance(keyword.value, ast.Call): - if isinstance(keyword.value.func, ast.Name): - if keyword.value.func.id == "get_httpx_client_kwargs": - return True - elif isinstance(keyword.value.func, ast.Attribute): - if keyword.value.func.attr == "get_httpx_client_kwargs": - return True - - return False - - def _variable_contains_get_httpx_client_kwargs(self, var_name: str) -> bool: - """Check if a variable was built using get_httpx_client_kwargs().""" - # Check if we've tracked this variable as containing httpx kwargs - if var_name in self.variables_with_httpx_kwargs: - return True - - # Fallback: Simple heuristic based on naming patterns - # This handles cases where the variable assignment might be complex - if "client_kwargs" in var_name.lower() or "httpx_kwargs" in var_name.lower(): - return True - - return False - - -def check_file(filepath: Path) -> List[LintViolation]: - """Check a single Python file for httpx.Client() violations.""" - try: - with open(filepath, "r", encoding="utf-8") as f: - content = f.read() - - tree = ast.parse(content, filename=str(filepath)) - checker = HttpxClientChecker(str(filepath)) - checker.visit(tree) - return checker.violations - - except SyntaxError: - # Skip files with syntax errors - return [] - except Exception as e: - print(f"Error checking {filepath}: {e}", file=sys.stderr) - return [] - - -def main(): - """Main function to run the linter.""" - if len(sys.argv) > 1: - paths = [Path(p) for p in sys.argv[1:]] - else: - # Default to checking src and tests directories - paths = [Path("src"), Path("tests")] - - all_violations = [] - - for path in paths: - if path.is_file() and path.suffix == ".py": - violations = check_file(path) - all_violations.extend(violations) - elif path.is_dir(): - for py_file in path.rglob("*.py"): - violations = check_file(py_file) - all_violations.extend(violations) - - # Report violations - if all_violations: - for violation in all_violations: - print( - f"{violation.filename}:{violation.line}:{violation.column}: {violation.rule_code} {violation.message}" - ) - sys.exit(1) - else: - print("No httpx.Client() violations found.") - sys.exit(0) - - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 +"""Custom linter to check for httpx.Client() usage. + +This script checks for direct usage of httpx.Client() without using the +get_httpx_client_kwargs() function, which is required for proper SSL +and proxy configuration in the UiPath Python SDK. +""" + +import ast +import sys +from pathlib import Path +from typing import NamedTuple + + +class LintViolation(NamedTuple): + """Represents a linting violation.""" + + filename: str + line: int + column: int + message: str + rule_code: str + + +class HttpxClientChecker(ast.NodeVisitor): + """AST visitor to check for httpx.Client() usage violations.""" + + def __init__(self, filename: str): + """Initialize the checker with a filename. + + Args: + filename: The path to the file being checked. + """ + self.filename = filename + self.violations: list[LintViolation] = [] + self.has_httpx_import = False + self.has_get_httpx_client_kwargs_import = False + # Track variables that contain get_httpx_client_kwargs + self.variables_with_httpx_kwargs: set[str] = set() + + def visit_Import(self, node: ast.Import) -> None: + """Check for httpx imports.""" + for alias in node.names: + if alias.name == "httpx": + self.has_httpx_import = True + self.generic_visit(node) + + def visit_ImportFrom(self, node: ast.ImportFrom) -> None: + """Check for imports from httpx or get_httpx_client_kwargs.""" + if node.module == "httpx": + self.has_httpx_import = True + elif node.module and "get_httpx_client_kwargs" in [ + alias.name for alias in (node.names or []) + ]: + self.has_get_httpx_client_kwargs_import = True + self.generic_visit(node) + + def visit_Assign(self, node: ast.Assign) -> None: + """Track variable assignments that use get_httpx_client_kwargs.""" + if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name): + var_name = node.targets[0].id + if self._assignment_uses_get_httpx_client_kwargs(node.value): + self.variables_with_httpx_kwargs.add(var_name) + self.generic_visit(node) + + def _assignment_uses_get_httpx_client_kwargs(self, value_node: ast.AST) -> bool: + """Check if an assignment value uses get_httpx_client_kwargs.""" + if isinstance(value_node, ast.Call): + # Direct call: var = get_httpx_client_kwargs() + if isinstance(value_node.func, ast.Name): + if value_node.func.id == "get_httpx_client_kwargs": + return True + elif isinstance(value_node.func, ast.Attribute): + if value_node.func.attr == "get_httpx_client_kwargs": + return True + elif isinstance(value_node, ast.Dict): + # Dictionary that spreads get_httpx_client_kwargs: {..., **get_httpx_client_kwargs()} + for key in value_node.keys: + if key is None: # This is a **kwargs expansion + # Find corresponding value + idx = value_node.keys.index(key) + if idx < len(value_node.values): + spread_value = value_node.values[idx] + if isinstance(spread_value, ast.Call): + if isinstance(spread_value.func, ast.Name): + if spread_value.func.id == "get_httpx_client_kwargs": + return True + elif isinstance(spread_value.func, ast.Attribute): + if spread_value.func.attr == "get_httpx_client_kwargs": + return True + elif isinstance(spread_value, ast.Name): + # Spreading another variable that might contain httpx kwargs + if spread_value.id in self.variables_with_httpx_kwargs: + return True + return False + + def visit_Call(self, node: ast.Call) -> None: + """Check for httpx.Client() and httpx.AsyncClient() calls.""" + if self._is_httpx_client_call(node): + # Check if this is a proper usage with get_httpx_client_kwargs + if not self._is_using_get_httpx_client_kwargs(node): + client_type = self._get_client_type(node) + violation = LintViolation( + filename=self.filename, + line=node.lineno, + column=node.col_offset, + message=f"Use **get_httpx_client_kwargs() with {client_type}() - should be: {client_type}(**get_httpx_client_kwargs())", + rule_code="UIPATH001", + ) + self.violations.append(violation) + + self.generic_visit(node) + + def _is_httpx_client_call(self, node: ast.Call) -> bool: + """Check if the call is httpx.Client() or httpx.AsyncClient().""" + if isinstance(node.func, ast.Attribute): + if ( + isinstance(node.func.value, ast.Name) + and node.func.value.id == "httpx" + and node.func.attr in ("Client", "AsyncClient") + ): + return True + elif isinstance(node.func, ast.Name) and node.func.id in ( + "Client", + "AsyncClient", + ): + # This could be a direct Client/AsyncClient import, check if httpx is imported + return self.has_httpx_import + return False + + def _get_client_type(self, node: ast.Call) -> str: + """Get the client type name (Client or AsyncClient).""" + if isinstance(node.func, ast.Attribute): + return f"httpx.{node.func.attr}" + elif isinstance(node.func, ast.Name): + return node.func.id + return "httpx.Client" + + def _is_using_get_httpx_client_kwargs(self, node: ast.Call) -> bool: + """Check if the httpx.Client() call is using **get_httpx_client_kwargs().""" + # Check if there are any **kwargs that use get_httpx_client_kwargs directly + for keyword in node.keywords: + if keyword.arg is None: # This is a **kwargs expansion + if isinstance(keyword.value, ast.Call): + if isinstance(keyword.value.func, ast.Name): + if keyword.value.func.id == "get_httpx_client_kwargs": + return True + elif isinstance(keyword.value.func, ast.Attribute): + if keyword.value.func.attr == "get_httpx_client_kwargs": + return True + elif isinstance(keyword.value, ast.Name): + # Check if this variable might contain get_httpx_client_kwargs + # This handles cases like: **client_kwargs where client_kwargs was built from get_httpx_client_kwargs + var_name = keyword.value.id + if self._variable_contains_get_httpx_client_kwargs(var_name): + return True + + # Also check if it's the ONLY argument and it's **get_httpx_client_kwargs() + # This handles cases like: httpx.Client(**get_httpx_client_kwargs()) + if len(node.args) == 0 and len(node.keywords) == 1: + keyword = node.keywords[0] + if keyword.arg is None and isinstance(keyword.value, ast.Call): + if isinstance(keyword.value.func, ast.Name): + if keyword.value.func.id == "get_httpx_client_kwargs": + return True + elif isinstance(keyword.value.func, ast.Attribute): + if keyword.value.func.attr == "get_httpx_client_kwargs": + return True + + return False + + def _variable_contains_get_httpx_client_kwargs(self, var_name: str) -> bool: + """Check if a variable was built using get_httpx_client_kwargs().""" + # Check if we've tracked this variable as containing httpx kwargs + if var_name in self.variables_with_httpx_kwargs: + return True + + # Fallback: Simple heuristic based on naming patterns + # This handles cases where the variable assignment might be complex + if "client_kwargs" in var_name.lower() or "httpx_kwargs" in var_name.lower(): + return True + + return False + + +def check_file(filepath: Path) -> list[LintViolation]: + """Check a single Python file for httpx.Client() violations.""" + try: + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + + tree = ast.parse(content, filename=str(filepath)) + checker = HttpxClientChecker(str(filepath)) + checker.visit(tree) + return checker.violations + + except SyntaxError: + # Skip files with syntax errors + return [] + except Exception as e: + print(f"Error checking {filepath}: {e}", file=sys.stderr) + return [] + + +def main(): + """Main function to run the linter.""" + if len(sys.argv) > 1: + paths = [Path(p) for p in sys.argv[1:]] + else: + # Default to checking src and tests directories + paths = [Path("src"), Path("tests")] + + all_violations = [] + + for path in paths: + if path.is_file() and path.suffix == ".py": + violations = check_file(path) + all_violations.extend(violations) + elif path.is_dir(): + for py_file in path.rglob("*.py"): + violations = check_file(py_file) + all_violations.extend(violations) + + # Report violations + if all_violations: + for violation in all_violations: + print( + f"{violation.filename}:{violation.line}:{violation.column}: {violation.rule_code} {violation.message}" + ) + sys.exit(1) + else: + print("No httpx.Client() violations found.") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/scripts/update_agents_md.py b/scripts/update_agents_md.py index b09de6576..3e83ddcec 100644 --- a/scripts/update_agents_md.py +++ b/scripts/update_agents_md.py @@ -11,14 +11,14 @@ import sys from io import StringIO from pathlib import Path -from typing import Any, Dict +from typing import Any sys.path.insert(0, str(Path(__file__).parent.parent / "src")) import click -def get_command_help(command: click.Command, command_name: str) -> Dict[str, Any]: +def get_command_help(command: click.Command, command_name: str) -> dict[str, Any]: """Extract help information from a Click command. Args: @@ -548,7 +548,7 @@ def generate_service_cli_docs() -> str: def _write_command_doc( - output: StringIO, cmd_info: Dict[str, Any], *path_parts: str + output: StringIO, cmd_info: dict[str, Any], *path_parts: str ) -> None: """Write command documentation to output stream. diff --git a/src/uipath/_cli/_auth/_auth_server.py b/src/uipath/_cli/_auth/_auth_server.py index b6b076015..4433b1c20 100644 --- a/src/uipath/_cli/_auth/_auth_server.py +++ b/src/uipath/_cli/_auth/_auth_server.py @@ -5,7 +5,6 @@ import socketserver import threading import time -from typing import Optional # Server port PORT = 6234 @@ -113,10 +112,10 @@ def __init__(self, port=6234, redirect_uri=None, client_id=None): self.port = port self.redirect_uri = redirect_uri self.client_id = client_id - self.httpd: Optional[socketserver.TCPServer] = None + self.httpd: socketserver.TCPServer | None = None self.token_data = None self.should_shutdown = False - self.token_received_event: Optional[asyncio.Event] = None + self.token_received_event: asyncio.Event | None = None self.loop = None def token_received_callback(self, token_data): diff --git a/src/uipath/_cli/_auth/_auth_service.py b/src/uipath/_cli/_auth/_auth_service.py index feca3074d..6f3537e23 100644 --- a/src/uipath/_cli/_auth/_auth_service.py +++ b/src/uipath/_cli/_auth/_auth_service.py @@ -1,7 +1,6 @@ import asyncio import os import webbrowser -from typing import Optional from uipath._cli._auth._auth_server import HTTPServer from uipath._cli._auth._oidc_utils import OidcUtils @@ -21,14 +20,14 @@ class AuthService: def __init__( self, - environment: Optional[str], + environment: str | None, *, force: bool, - client_id: Optional[str] = None, - client_secret: Optional[str] = None, - base_url: Optional[str] = None, - tenant: Optional[str] = None, - scope: Optional[str] = None, + client_id: str | None = None, + client_secret: str | None = None, + base_url: str | None = None, + tenant: str | None = None, + scope: str | None = None, ): self._force = force self._console = ConsoleLogger() diff --git a/src/uipath/_cli/_auth/_oidc_utils.py b/src/uipath/_cli/_auth/_oidc_utils.py index 7b471c6b8..b6acdd6a5 100644 --- a/src/uipath/_cli/_auth/_oidc_utils.py +++ b/src/uipath/_cli/_auth/_oidc_utils.py @@ -2,7 +2,6 @@ import hashlib import json import os -from typing import Optional from urllib.parse import urlencode, urlparse import httpx @@ -29,7 +28,7 @@ def get_state_param() -> str: return base64.urlsafe_b64encode(os.urandom(32)).decode("utf-8").rstrip("=") -def _get_version_from_api(domain: str) -> Optional[str]: +def _get_version_from_api(domain: str) -> str | None: """Fetch the version from the UiPath orchestrator API. Args: @@ -125,7 +124,7 @@ def is_free(port: int) -> bool: return next((p for p in candidates if is_free(p)), None) @classmethod - def get_auth_config(cls, domain: Optional[str] = None) -> AuthConfig: + def get_auth_config(cls, domain: str | None = None) -> AuthConfig: """Get the appropriate auth configuration based on domain. Args: diff --git a/src/uipath/_cli/_auth/_portal_service.py b/src/uipath/_cli/_auth/_portal_service.py index c3c4e5c48..d50b3a38d 100644 --- a/src/uipath/_cli/_auth/_portal_service.py +++ b/src/uipath/_cli/_auth/_portal_service.py @@ -1,5 +1,4 @@ import time -from typing import Optional import click import httpx @@ -22,19 +21,19 @@ class PortalService: """Service for interacting with the UiPath Portal API.""" - access_token: Optional[str] = None - prt_id: Optional[str] = None + access_token: str | None = None + prt_id: str | None = None domain: str - selected_tenant: Optional[str] = None + selected_tenant: str | None = None - _client: Optional[httpx.Client] = None - _tenants_and_organizations: Optional[TenantsAndOrganizationInfoResponse] = None + _client: httpx.Client | None = None + _tenants_and_organizations: TenantsAndOrganizationInfoResponse | None = None def __init__( self, domain: str, - access_token: Optional[str] = None, - prt_id: Optional[str] = None, + access_token: str | None = None, + prt_id: str | None = None, ): self.domain = domain self.access_token = access_token @@ -206,7 +205,7 @@ def _retrieve_tenant( return self._set_tenant(tenant, organization) - def resolve_tenant_info(self, tenant: Optional[str] = None): + def resolve_tenant_info(self, tenant: str | None = None): if tenant: return self._retrieve_tenant(tenant) return self._select_tenant() diff --git a/src/uipath/_cli/_auth/_url_utils.py b/src/uipath/_cli/_auth/_url_utils.py index 9cb440cdb..844968762 100644 --- a/src/uipath/_cli/_auth/_url_utils.py +++ b/src/uipath/_cli/_auth/_url_utils.py @@ -1,5 +1,5 @@ import os -from typing import Optional, Tuple +from typing import Tuple from urllib.parse import urlparse from .._utils._console import ConsoleLogger @@ -7,7 +7,7 @@ console = ConsoleLogger() -def resolve_domain(base_url: Optional[str], environment: Optional[str]) -> str: +def resolve_domain(base_url: str | None, environment: str | None) -> str: """Resolve the UiPath domain, giving priority to base_url when valid. Args: @@ -55,7 +55,7 @@ def build_service_url(domain: str, path: str) -> str: return f"{domain}{path}" -def extract_org_tenant(uipath_url: str) -> Tuple[Optional[str], Optional[str]]: +def extract_org_tenant(uipath_url: str) -> Tuple[str | None, str | None]: """Extract organization and tenant from a UiPath URL. Accepts values like: diff --git a/src/uipath/_cli/_auth/_utils.py b/src/uipath/_cli/_auth/_utils.py index 2202f27a1..cd3628075 100644 --- a/src/uipath/_cli/_auth/_utils.py +++ b/src/uipath/_cli/_auth/_utils.py @@ -1,7 +1,6 @@ import json import os from pathlib import Path -from typing import Optional from ..._utils._auth import parse_access_token from ...platform.common import TokenData @@ -22,7 +21,7 @@ def get_auth_data() -> TokenData: return TokenData.model_validate(json.load(open(auth_file))) -def get_parsed_token_data(token_data: Optional[TokenData] = None) -> AccessTokenData: +def get_parsed_token_data(token_data: TokenData | None = None) -> AccessTokenData: if not token_data: token_data = get_auth_data() return parse_access_token(token_data.access_token) diff --git a/src/uipath/_cli/_debug/_bridge.py b/src/uipath/_cli/_debug/_bridge.py index 651260c8f..24bf1f8c3 100644 --- a/src/uipath/_cli/_debug/_bridge.py +++ b/src/uipath/_cli/_debug/_bridge.py @@ -73,7 +73,7 @@ def __init__(self, verbose: bool = True): Args: verbose: If True, show state updates. If False, only show breakpoints. """ - self.console = Console() + self.console = Console(force_terminal=True) self.verbose = verbose self.state = DebuggerState() diff --git a/src/uipath/_cli/_evals/_console_progress_reporter.py b/src/uipath/_cli/_evals/_console_progress_reporter.py index 552d79ecb..9028a2397 100644 --- a/src/uipath/_cli/_evals/_console_progress_reporter.py +++ b/src/uipath/_cli/_evals/_console_progress_reporter.py @@ -1,7 +1,7 @@ """Console progress reporter for evaluation runs with line-by-line output.""" import logging -from typing import Any, Dict +from typing import Any from rich.console import Console from rich.rule import Rule @@ -26,9 +26,9 @@ class ConsoleProgressReporter: def __init__(self): self.console = Console() - self.evaluators: Dict[str, BaseEvaluator[Any, Any, Any]] = {} + self.evaluators: dict[str, BaseEvaluator[Any, Any, Any]] = {} self.display_started = False - self.eval_results_by_name: Dict[str, list[Any]] = {} + self.eval_results_by_name: dict[str, list[Any]] = {} def _convert_score_to_numeric(self, eval_result) -> float: """Convert evaluation result score to numeric value.""" diff --git a/src/uipath/_cli/_evals/_evaluator_factory.py b/src/uipath/_cli/_evals/_evaluator_factory.py index c9d07cf63..8d6e5d4f8 100644 --- a/src/uipath/_cli/_evals/_evaluator_factory.py +++ b/src/uipath/_cli/_evals/_evaluator_factory.py @@ -1,7 +1,7 @@ import importlib.util import sys from pathlib import Path -from typing import Any, Dict +from typing import Any from pydantic import TypeAdapter @@ -73,7 +73,7 @@ class EvaluatorFactory: """Factory class for creating evaluator instances based on configuration.""" @staticmethod - def _prepare_evaluator_config(data: Dict[str, Any]) -> Dict[str, Any]: + def _prepare_evaluator_config(data: dict[str, Any]) -> dict[str, Any]: """Prepare evaluator config by merging top-level fields into evaluatorConfig. This allows flexibility in specifying fields like 'name' and 'description' either at the @@ -106,7 +106,7 @@ def _prepare_evaluator_config(data: Dict[str, Any]) -> Dict[str, Any]: @classmethod def create_evaluator( - cls, data: Dict[str, Any], evaluators_dir: Path | None = None + cls, data: dict[str, Any], evaluators_dir: Path | None = None ) -> BaseEvaluator[Any, Any, Any]: if data.get("version", None) == "1.0": return cls._create_evaluator_internal(data, evaluators_dir) @@ -115,7 +115,7 @@ def create_evaluator( @staticmethod def _create_evaluator_internal( - data: Dict[str, Any], + data: dict[str, Any], evaluators_dir: Path | None = None, ) -> BaseEvaluator[Any, Any, Any]: # check custom evaluator @@ -165,7 +165,7 @@ def _create_evaluator_internal( raise ValueError(f"Unknown evaluator configuration: {config}") @staticmethod - def _create_contains_evaluator(data: Dict[str, Any]) -> ContainsEvaluator: + def _create_contains_evaluator(data: dict[str, Any]) -> ContainsEvaluator: evaluator_id = data.get("id") if not evaluator_id or not isinstance(evaluator_id, str): raise ValueError("Evaluator 'id' must be a non-empty string") @@ -178,7 +178,7 @@ def _create_contains_evaluator(data: Dict[str, Any]) -> ContainsEvaluator: @staticmethod def _create_coded_evaluator_internal( - data: Dict[str, Any], + data: dict[str, Any], file_path_str: str, class_name: str, evaluators_dir: Path | None = None, @@ -260,7 +260,7 @@ def _create_coded_evaluator_internal( @staticmethod def _create_exact_match_evaluator( - data: Dict[str, Any], + data: dict[str, Any], ) -> ExactMatchEvaluator: return TypeAdapter(ExactMatchEvaluator).validate_python( { @@ -271,7 +271,7 @@ def _create_exact_match_evaluator( @staticmethod def _create_json_similarity_evaluator( - data: Dict[str, Any], + data: dict[str, Any], ) -> JsonSimilarityEvaluator: return TypeAdapter(JsonSimilarityEvaluator).validate_python( { @@ -282,7 +282,7 @@ def _create_json_similarity_evaluator( @staticmethod def _create_llm_judge_output_evaluator( - data: Dict[str, Any], + data: dict[str, Any], ) -> LLMJudgeOutputEvaluator: return TypeAdapter(LLMJudgeOutputEvaluator).validate_python( { @@ -293,7 +293,7 @@ def _create_llm_judge_output_evaluator( @staticmethod def _create_llm_judge_strict_json_similarity_output_evaluator( - data: Dict[str, Any], + data: dict[str, Any], ) -> LLMJudgeStrictJSONSimilarityOutputEvaluator: return TypeAdapter(LLMJudgeStrictJSONSimilarityOutputEvaluator).validate_python( { @@ -304,7 +304,7 @@ def _create_llm_judge_strict_json_similarity_output_evaluator( @staticmethod def _create_trajectory_evaluator( - data: Dict[str, Any], + data: dict[str, Any], ) -> LLMJudgeTrajectoryEvaluator: return TypeAdapter(LLMJudgeTrajectoryEvaluator).validate_python( { @@ -315,7 +315,7 @@ def _create_trajectory_evaluator( @staticmethod def _create_tool_call_args_evaluator( - data: Dict[str, Any], + data: dict[str, Any], ) -> ToolCallArgsEvaluator: return TypeAdapter(ToolCallArgsEvaluator).validate_python( { @@ -326,7 +326,7 @@ def _create_tool_call_args_evaluator( @staticmethod def _create_tool_call_count_evaluator( - data: Dict[str, Any], + data: dict[str, Any], ) -> ToolCallCountEvaluator: return TypeAdapter(ToolCallCountEvaluator).validate_python( { @@ -337,7 +337,7 @@ def _create_tool_call_count_evaluator( @staticmethod def _create_tool_call_order_evaluator( - data: Dict[str, Any], + data: dict[str, Any], ) -> ToolCallOrderEvaluator: return TypeAdapter(ToolCallOrderEvaluator).validate_python( { @@ -348,7 +348,7 @@ def _create_tool_call_order_evaluator( @staticmethod def _create_tool_call_output_evaluator( - data: Dict[str, Any], + data: dict[str, Any], ) -> ToolCallOutputEvaluator: return TypeAdapter(ToolCallOutputEvaluator).validate_python( { @@ -359,7 +359,7 @@ def _create_tool_call_output_evaluator( @staticmethod def _create_llm_judge_simulation_trajectory_evaluator( - data: Dict[str, Any], + data: dict[str, Any], ) -> LLMJudgeTrajectorySimulationEvaluator: return TypeAdapter(LLMJudgeTrajectorySimulationEvaluator).validate_python( { @@ -370,7 +370,7 @@ def _create_llm_judge_simulation_trajectory_evaluator( @staticmethod def _create_legacy_evaluator_internal( - data: Dict[str, Any], + data: dict[str, Any], ) -> LegacyBaseEvaluator[Any]: """Create an evaluator instance from configuration data. diff --git a/src/uipath/_cli/_evals/_helpers.py b/src/uipath/_cli/_evals/_helpers.py index 1784d049c..d52e3c476 100644 --- a/src/uipath/_cli/_evals/_helpers.py +++ b/src/uipath/_cli/_evals/_helpers.py @@ -6,7 +6,7 @@ import re import sys from pathlib import Path -from typing import Any, Optional +from typing import Any import click @@ -35,7 +35,7 @@ def to_kebab_case(text: str) -> str: return re.sub(r"(? Optional[Path]: +def find_evaluator_file(filename: str) -> Path | None: """Find the evaluator file in evals/evaluators/custom folder.""" custom_evaluators_path = Path.cwd() / EVALS_DIRECTORY_NAME / "evaluators" / "custom" @@ -49,7 +49,7 @@ def find_evaluator_file(filename: str) -> Optional[Path]: return None -def find_base_evaluator_class(file_path: Path) -> Optional[str]: +def find_base_evaluator_class(file_path: Path) -> str | None: """Parse the Python file and find the class that inherits from BaseEvaluator.""" try: with open(file_path, "r") as f: @@ -73,7 +73,7 @@ def find_base_evaluator_class(file_path: Path) -> Optional[str]: return None -def load_evaluator_class(file_path: Path, class_name: str) -> Optional[type]: +def load_evaluator_class(file_path: Path, class_name: str) -> type | None: """Dynamically load the evaluator class from the file.""" try: parent_dir = str(file_path.parent) diff --git a/src/uipath/_cli/_evals/_models/_evaluation_set.py b/src/uipath/_cli/_evals/_models/_evaluation_set.py index fbd280bc3..74eba2670 100644 --- a/src/uipath/_cli/_evals/_models/_evaluation_set.py +++ b/src/uipath/_cli/_evals/_models/_evaluation_set.py @@ -1,5 +1,5 @@ from enum import Enum, IntEnum -from typing import Annotated, Any, Dict, List, Literal, Optional, Union +from typing import Annotated, Any, Literal, Union from pydantic import BaseModel, ConfigDict, Field from pydantic.alias_generators import to_camel @@ -70,12 +70,12 @@ class ModelSettings(BaseModel): """Model Generation Parameters.""" model: str = Field(..., alias="model") - temperature: Optional[float] = Field(default=None, alias="temperature") - top_p: Optional[float] = Field(default=None, alias="topP") - top_k: Optional[int] = Field(default=None, alias="topK") - frequency_penalty: Optional[float] = Field(default=None, alias="frequencyPenalty") - presence_penalty: Optional[float] = Field(default=None, alias="presencePenalty") - max_tokens: Optional[int] = Field(default=None, alias="maxTokens") + temperature: float | None = Field(default=None, alias="temperature") + top_p: float | None = Field(default=None, alias="topP") + top_k: int | None = Field(default=None, alias="topK") + frequency_penalty: float | None = Field(default=None, alias="frequencyPenalty") + presence_penalty: float | None = Field(default=None, alias="presencePenalty") + max_tokens: int | None = Field(default=None, alias="maxTokens") class LLMMockingStrategy(BaseMockingStrategy): @@ -84,7 +84,7 @@ class LLMMockingStrategy(BaseMockingStrategy): tools_to_simulate: list[EvaluationSimulationTool] = Field( ..., alias="toolsToSimulate" ) - model: Optional[ModelSettings] = Field(None, alias="model") + model: ModelSettings | None = Field(None, alias="model") model_config = ConfigDict( validate_by_name=True, validate_by_alias=True, extra="allow" @@ -93,7 +93,7 @@ class LLMMockingStrategy(BaseMockingStrategy): class InputMockingStrategy(BaseModel): prompt: str = Field(..., alias="prompt") - model: Optional[ModelSettings] = Field(None, alias="model") + model: ModelSettings | None = Field(None, alias="model") model_config = ConfigDict( validate_by_name=True, validate_by_alias=True, extra="allow" @@ -101,8 +101,8 @@ class InputMockingStrategy(BaseModel): class MockingArgument(BaseModel): - args: List[Any] = Field(default_factory=lambda: [], alias="args") - kwargs: Dict[str, Any] = Field(default_factory=lambda: {}, alias="kwargs") + args: list[Any] = Field(default_factory=lambda: [], alias="args") + kwargs: dict[str, Any] = Field(default_factory=lambda: {}, alias="kwargs") class MockingAnswerType(str, Enum): @@ -118,12 +118,12 @@ class MockingAnswer(BaseModel): class MockingBehavior(BaseModel): function: str = Field(..., alias="function") arguments: MockingArgument = Field(..., alias="arguments") - then: List[MockingAnswer] = Field(..., alias="then") + then: list[MockingAnswer] = Field(..., alias="then") class MockitoMockingStrategy(BaseMockingStrategy): type: Literal[MockingStrategyType.MOCKITO] = MockingStrategyType.MOCKITO - behaviors: List[MockingBehavior] = Field(..., alias="config") + behaviors: list[MockingBehavior] = Field(..., alias="config") model_config = ConfigDict( validate_by_name=True, validate_by_alias=True, extra="allow" @@ -153,16 +153,16 @@ class EvaluationItem(BaseModel): model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) id: str name: str - inputs: Dict[str, Any] + inputs: dict[str, Any] evaluation_criterias: dict[str, dict[str, Any] | None] = Field( ..., alias="evaluationCriterias" ) expected_agent_behavior: str = Field(default="", alias="expectedAgentBehavior") - mocking_strategy: Optional[MockingStrategy] = Field( + mocking_strategy: MockingStrategy | None = Field( default=None, alias="mockingStrategy", ) - input_mocking_strategy: Optional[InputMockingStrategy] = Field( + input_mocking_strategy: InputMockingStrategy | None = Field( default=None, alias="inputMockingStrategy", ) @@ -177,18 +177,18 @@ class LegacyEvaluationItem(BaseModel): id: str name: str - inputs: Dict[str, Any] - expected_output: Dict[str, Any] + inputs: dict[str, Any] + expected_output: dict[str, Any] expected_agent_behavior: str = Field(default="", alias="expectedAgentBehavior") eval_set_id: str = Field(alias="evalSetId") created_at: str = Field(alias="createdAt") updated_at: str = Field(alias="updatedAt") - simulate_input: Optional[bool] = Field(default=None, alias="simulateInput") - input_generation_instructions: Optional[str] = Field( + simulate_input: bool | None = Field(default=None, alias="simulateInput") + input_generation_instructions: str | None = Field( default=None, alias="inputGenerationInstructions" ) - simulate_tools: Optional[bool] = Field(default=None, alias="simulateInput") - simulation_instructions: Optional[str] = Field( + simulate_tools: bool | None = Field(default=None, alias="simulateInput") + simulation_instructions: str | None = Field( default=None, alias="simulationInstructions" ) tools_to_simulate: list[EvaluationSimulationTool] = Field( @@ -206,11 +206,11 @@ class EvaluationSet(BaseModel): id: str name: str version: Literal["1.0"] = "1.0" - evaluator_refs: List[str] = Field(default_factory=list) - evaluator_configs: List[EvaluatorReference] = Field( + evaluator_refs: list[str] = Field(default_factory=list) + evaluator_configs: list[EvaluatorReference] = Field( default_factory=list, alias="evaluatorConfigs" ) - evaluations: List[EvaluationItem] = Field(default_factory=list) + evaluations: list[EvaluationItem] = Field(default_factory=list) def extract_selected_evals(self, eval_ids) -> None: selected_evals: list[EvaluationItem] = [] @@ -231,15 +231,15 @@ class LegacyEvaluationSet(BaseModel): id: str file_name: str = Field(..., alias="fileName") - evaluator_refs: List[str] = Field(default_factory=list) - evaluator_configs: List[EvaluatorReference] = Field( + evaluator_refs: list[str] = Field(default_factory=list) + evaluator_configs: list[EvaluatorReference] = Field( default_factory=list, alias="evaluatorConfigs" ) - evaluations: List[LegacyEvaluationItem] = Field(default_factory=list) + evaluations: list[LegacyEvaluationItem] = Field(default_factory=list) name: str batch_size: int = Field(10, alias="batchSize") timeout_minutes: int = Field(default=20, alias="timeoutMinutes") - model_settings: List[Dict[str, Any]] = Field( + model_settings: list[dict[str, Any]] = Field( default_factory=list, alias="modelSettings" ) created_at: str = Field(alias="createdAt") diff --git a/src/uipath/_cli/_evals/_models/_output.py b/src/uipath/_cli/_evals/_models/_output.py index 772827990..831c6f781 100644 --- a/src/uipath/_cli/_evals/_models/_output.py +++ b/src/uipath/_cli/_evals/_models/_output.py @@ -1,6 +1,6 @@ import logging from collections import defaultdict -from typing import Any, Dict, List, Optional +from typing import Any from opentelemetry.sdk.trace import ReadableSpan from pydantic import BaseModel, ConfigDict, model_serializer @@ -46,8 +46,8 @@ class EvaluationResultDto(BaseModel): model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) score: float - details: Optional[str | BaseModel] = None - evaluation_time: Optional[float] = None + details: str | BaseModel | None = None + evaluation_time: float | None = None @model_serializer(mode="wrap") def serialize_model( @@ -92,8 +92,8 @@ class EvaluationRunResult(BaseModel): model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) evaluation_name: str - evaluation_run_results: List[EvaluationRunResultDto] - agent_execution_output: Optional[UiPathSerializableEvalRunExecutionOutput] = None + evaluation_run_results: list[EvaluationRunResultDto] + agent_execution_output: UiPathSerializableEvalRunExecutionOutput | None = None @property def score(self) -> float: @@ -109,7 +109,7 @@ class UiPathEvalOutput(BaseModel): model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) evaluation_set_name: str - evaluation_set_results: List[EvaluationRunResult] + evaluation_set_results: list[EvaluationRunResult] @property def score(self) -> float: @@ -124,9 +124,9 @@ def score(self) -> float: def calculate_final_score( self, - evaluator_weights: Dict[str, float] | None = None, + evaluator_weights: dict[str, float] | None = None, default_weight: float = 1.0, - ) -> tuple[float, Dict[str, float]]: + ) -> tuple[float, dict[str, float]]: """Aggregate evaluation results with deduplication and weighted scoring. This function performs the following steps: diff --git a/src/uipath/_cli/_evals/_progress_reporter.py b/src/uipath/_cli/_evals/_progress_reporter.py index f558fe985..c918c7b36 100644 --- a/src/uipath/_cli/_evals/_progress_reporter.py +++ b/src/uipath/_cli/_evals/_progress_reporter.py @@ -6,7 +6,7 @@ import os import uuid from datetime import datetime, timezone -from typing import Any, Dict, List +from typing import Any from urllib.parse import urlparse from opentelemetry import trace @@ -93,12 +93,12 @@ def __init__(self, spans_exporter: LlmOpsHttpExporter): "Cannot report data to StudioWeb. Please set UIPATH_PROJECT_ID." ) - self.eval_set_run_ids: Dict[str, str] = {} - self.evaluators: Dict[str, Any] = {} - self.evaluator_scores: Dict[str, List[float]] = {} - self.eval_run_ids: Dict[str, str] = {} - self.is_coded_eval: Dict[str, bool] = {} # Track coded vs legacy per execution - self.eval_spans: Dict[ + self.eval_set_run_ids: dict[str, str] = {} + self.evaluators: dict[str, Any] = {} + self.evaluator_scores: dict[str, list[float]] = {} + self.eval_run_ids: dict[str, str] = {} + self.is_coded_eval: dict[str, bool] = {} # Track coded vs legacy per execution + self.eval_spans: dict[ str, list[Any] ] = {} # Store spans per execution for usage metrics self.eval_set_execution_id: str | None = ( @@ -153,7 +153,7 @@ def _get_endpoint_prefix(self) -> str: return "agentsruntime_/api/" def _is_coded_evaluator( - self, evaluators: List[BaseEvaluator[Any, Any, Any]] + self, evaluators: list[BaseEvaluator[Any, Any, Any]] ) -> bool: """Check if evaluators are coded (BaseEvaluator) vs legacy (LegacyBaseEvaluator). @@ -236,7 +236,7 @@ async def create_eval_set_run_sw( eval_set_id: str, agent_snapshot: StudioWebAgentSnapshot, no_of_evals: int, - evaluators: List[LegacyBaseEvaluator[Any]], + evaluators: list[LegacyBaseEvaluator[Any]], is_coded: bool = False, ) -> str: """Create a new evaluation set run in StudioWeb.""" @@ -1014,7 +1014,7 @@ async def _send_eval_run_trace( logger.warning(f"Failed to create eval run trace: {e}") async def _send_evaluator_traces( - self, eval_run_id: str, eval_results: List[EvalItemResult], spans: list[Any] + self, eval_run_id: str, eval_results: list[EvalItemResult], spans: list[Any] ) -> None: """Send trace spans for all evaluators. diff --git a/src/uipath/_cli/_evals/_runtime.py b/src/uipath/_cli/_evals/_runtime.py index 57f0abeec..782382770 100644 --- a/src/uipath/_cli/_evals/_runtime.py +++ b/src/uipath/_cli/_evals/_runtime.py @@ -5,7 +5,7 @@ from collections import defaultdict from pathlib import Path from time import time -from typing import Any, Optional, Sequence +from typing import Any, Sequence import coverage from opentelemetry import context as context_api @@ -87,7 +87,7 @@ def get_spans(self, execution_id: str) -> list[ReadableSpan]: """Retrieve spans for a given execution id.""" return self._spans.get(execution_id, []) - def clear(self, execution_id: Optional[str] = None) -> None: + def clear(self, execution_id: str | None = None) -> None: """Clear stored spans for one or all executions.""" if execution_id: self._spans.pop(execution_id, None) @@ -106,7 +106,7 @@ def __init__(self, span_exporter: SpanExporter, collector: ExecutionSpanCollecto self.collector = collector def on_start( - self, span: Span, parent_context: Optional[context_api.Context] = None + self, span: Span, parent_context: context_api.Context | None = None ) -> None: super().on_start(span, parent_context) @@ -132,7 +132,7 @@ def get_logs(self, execution_id: str) -> list[logging.LogRecord]: log_handler = self._log_handlers.get(execution_id) return log_handler.buffer if log_handler else [] - def clear(self, execution_id: Optional[str] = None) -> None: + def clear(self, execution_id: str | None = None) -> None: """Clear stored spans for one or all executions.""" if execution_id: self._log_handlers.pop(execution_id, None) @@ -143,12 +143,12 @@ def clear(self, execution_id: Optional[str] = None) -> None: class UiPathEvalContext: """Context used for evaluation runs.""" - entrypoint: Optional[str] = None - no_report: Optional[bool] = False - workers: Optional[int] = 1 - eval_set: Optional[str] = None - eval_ids: Optional[list[str]] = None - eval_set_run_id: Optional[str] = None + entrypoint: str | None = None + no_report: bool | None = False + workers: int | None = 1 + eval_set: str | None = None + eval_ids: list[str] | None = None + eval_set_run_id: str | None = None verbose: bool = False enable_mocker_cache: bool = False report_coverage: bool = False @@ -178,7 +178,7 @@ def __init__( self.logs_exporter: ExecutionLogsExporter = ExecutionLogsExporter() self.execution_id = str(uuid.uuid4()) - self.schema: Optional[UiPathRuntimeSchema] = None + self.schema: UiPathRuntimeSchema | None = None self.coverage = coverage.Coverage(branch=True) async def __aenter__(self) -> "UiPathEvalRuntime": diff --git a/src/uipath/_cli/_evals/_span_collection.py b/src/uipath/_cli/_evals/_span_collection.py index ba00eba8f..7ade2308b 100644 --- a/src/uipath/_cli/_evals/_span_collection.py +++ b/src/uipath/_cli/_evals/_span_collection.py @@ -1,5 +1,4 @@ from collections import defaultdict -from typing import Dict, List, Optional from opentelemetry.sdk.trace import ReadableSpan, Span @@ -9,15 +8,15 @@ class ExecutionSpanCollector: def __init__(self): # { execution_id -> list of spans } - self._spans: Dict[str, List[ReadableSpan]] = defaultdict(list) + self._spans: dict[str, list[ReadableSpan]] = defaultdict(list) def add_span(self, span: Span, execution_id: str) -> None: self._spans[execution_id].append(span) - def get_spans(self, execution_id: str) -> List[ReadableSpan]: + def get_spans(self, execution_id: str) -> list[ReadableSpan]: return self._spans.get(execution_id, []) - def clear(self, execution_id: Optional[str] = None) -> None: + def clear(self, execution_id: str | None = None) -> None: if execution_id: self._spans.pop(execution_id, None) else: diff --git a/src/uipath/_cli/_evals/mocks/cache_manager.py b/src/uipath/_cli/_evals/mocks/cache_manager.py index 06cc08d10..fa92d6f80 100644 --- a/src/uipath/_cli/_evals/mocks/cache_manager.py +++ b/src/uipath/_cli/_evals/mocks/cache_manager.py @@ -3,19 +3,19 @@ import hashlib import json from pathlib import Path -from typing import Any, Dict, Optional, Set +from typing import Any class CacheManager: """Manages caching for LLM and input mocker responses.""" - def __init__(self, cache_dir: Optional[Path] = None): + def __init__(self, cache_dir: Path | None = None): """Initialize the cache manager with in-memory cache.""" self.cache_dir = cache_dir or (Path.cwd() / ".uipath" / "eval_cache") - self._memory_cache: Dict[str, Any] = {} - self._dirty_keys: Set[str] = set() + self._memory_cache: dict[str, Any] = {} + self._dirty_keys: set[str] = set() - def _compute_cache_key(self, cache_key_data: Dict[str, Any]) -> str: + def _compute_cache_key(self, cache_key_data: dict[str, Any]) -> str: """Compute a hash from cache key data.""" serialized = json.dumps(cache_key_data, sort_keys=True) return hashlib.sha256(serialized.encode()).hexdigest() @@ -23,7 +23,7 @@ def _compute_cache_key(self, cache_key_data: Dict[str, Any]) -> str: def _get_cache_key_string( self, mocker_type: str, - cache_key_data: Dict[str, Any], + cache_key_data: dict[str, Any], function_name: str, ) -> str: """Generate unique cache key string for memory lookup.""" @@ -40,9 +40,9 @@ def _get_cache_path( def get( self, mocker_type: str, - cache_key_data: Dict[str, Any], + cache_key_data: dict[str, Any], function_name: str, - ) -> Optional[Any]: + ) -> Any: """Retrieve a cached response from memory first, then disk.""" cache_key_string = self._get_cache_key_string( mocker_type, cache_key_data, function_name @@ -67,7 +67,7 @@ def get( def set( self, mocker_type: str, - cache_key_data: Dict[str, Any], + cache_key_data: dict[str, Any], response: Any, function_name: str, ) -> None: diff --git a/src/uipath/_cli/_evals/mocks/input_mocker.py b/src/uipath/_cli/_evals/mocks/input_mocker.py index 040064ca3..6057e1207 100644 --- a/src/uipath/_cli/_evals/mocks/input_mocker.py +++ b/src/uipath/_cli/_evals/mocks/input_mocker.py @@ -2,7 +2,7 @@ import json from datetime import datetime -from typing import Any, Dict +from typing import Any from uipath._cli._evals._models._evaluation_set import EvaluationItem from uipath.platform import UiPath @@ -55,8 +55,8 @@ def get_input_mocking_prompt( @traced(name="__mocker__", recording=False) async def generate_llm_input( evaluation_item: EvaluationItem, - input_schema: Dict[str, Any], -) -> Dict[str, Any]: + input_schema: dict[str, Any], +) -> dict[str, Any]: """Generate synthetic input using an LLM based on the evaluation context.""" from .mocks import cache_manager_context diff --git a/src/uipath/_cli/_evals/mocks/mocks.py b/src/uipath/_cli/_evals/mocks/mocks.py index a15111e8b..bc46e8f6b 100644 --- a/src/uipath/_cli/_evals/mocks/mocks.py +++ b/src/uipath/_cli/_evals/mocks/mocks.py @@ -2,7 +2,7 @@ import logging from contextvars import ContextVar -from typing import Any, Callable, Optional +from typing import Any, Callable from uipath._cli._evals._models._evaluation_set import EvaluationItem from uipath._cli._evals._span_collection import ExecutionSpanCollector @@ -11,24 +11,21 @@ from uipath._cli._evals.mocks.mocker_factory import MockerFactory # Context variables for evaluation items and mockers -evaluation_context: ContextVar[Optional[EvaluationItem]] = ContextVar( +evaluation_context: ContextVar[EvaluationItem | None] = ContextVar( "evaluation", default=None ) -mocker_context: ContextVar[Optional[Mocker]] = ContextVar("mocker", default=None) - +mocker_context: ContextVar[Mocker | None] = ContextVar("mocker", default=None) # Span collector for trace access during mocking -span_collector_context: ContextVar[Optional[ExecutionSpanCollector]] = ContextVar( +span_collector_context: ContextVar[ExecutionSpanCollector | None] = ContextVar( "span_collector", default=None ) # Execution ID for the current evaluation item -execution_id_context: ContextVar[Optional[str]] = ContextVar( - "execution_id", default=None -) +execution_id_context: ContextVar[str | None] = ContextVar("execution_id", default=None) # Cache manager for LLM and input mocker responses -cache_manager_context: ContextVar[Optional[CacheManager]] = ContextVar( +cache_manager_context: ContextVar[CacheManager | None] = ContextVar( "cache_manager", default=None ) diff --git a/src/uipath/_cli/_push/sw_file_handler.py b/src/uipath/_cli/_push/sw_file_handler.py index a03cf3cff..05fa58dab 100644 --- a/src/uipath/_cli/_push/sw_file_handler.py +++ b/src/uipath/_cli/_push/sw_file_handler.py @@ -4,7 +4,7 @@ import logging import os from datetime import datetime, timezone -from typing import AsyncIterator, Optional, Set +from typing import AsyncIterator import click @@ -54,7 +54,7 @@ def __init__( project_id: str, directory: str, include_uv_lock: bool = True, - studio_client: Optional[StudioClient] = None, + studio_client: StudioClient | None = None, ) -> None: """Initialize the SwFileHandler. @@ -68,11 +68,11 @@ def __init__( self.include_uv_lock = include_uv_lock self.console = ConsoleLogger() self._studio_client = studio_client or StudioClient(project_id) - self._project_structure: Optional[ProjectStructure] = None + self._project_structure: ProjectStructure | None = None def _get_folder_by_name( self, structure: ProjectStructure, folder_name: str - ) -> Optional[ProjectFolder]: + ) -> ProjectFolder | None: """Get a folder from the project structure by name. Args: @@ -155,7 +155,7 @@ async def _process_file_uploads( structural_migration = StructuralMigration( deleted_resources=[], added_resources=[], modified_resources=[] ) - processed_source_files: Set[str] = set() + processed_source_files: set[str] = set() updates: list[UpdateEvent] = [] for local_file in local_files: @@ -291,7 +291,7 @@ async def _process_file_uploads( def _collect_deleted_files( self, remote_files: dict[str, ProjectFile], - processed_source_file_ids: Set[str], + processed_source_file_ids: set[str], files_to_ignore: list[str] | None = None, directories_to_ignore: list[str] | None = None, ) -> set[str]: @@ -304,7 +304,7 @@ def _collect_deleted_files( Returns: Set of file IDs to delete """ - deleted_file_ids: Set[str] = set() + deleted_file_ids: set[str] = set() if not files_to_ignore: files_to_ignore = [] @@ -544,7 +544,7 @@ async def _process_file_sync( parent_path: str, destination_prefix: str, structural_migration: StructuralMigration, - processed_ids: Set[str], + processed_ids: set[str], ) -> None: """Process a single local file for upload or update to remote. @@ -609,7 +609,7 @@ async def _process_file_sync( def _collect_deleted_remote_files( self, remote_files: dict[str, ProjectFile], - processed_ids: Set[str], + processed_ids: set[str], destination_prefix: str, structural_migration: StructuralMigration, ) -> None: diff --git a/src/uipath/_cli/_templates/main.py.template b/src/uipath/_cli/_templates/main.py.template index ca1cab1bb..5292acf90 100644 --- a/src/uipath/_cli/_templates/main.py.template +++ b/src/uipath/_cli/_templates/main.py.template @@ -1,12 +1,11 @@ from dataclasses import dataclass -from typing import Optional @dataclass class EchoIn: message: str - repeat: Optional[int] = 1 - prefix: Optional[str] = None + repeat: int | None = 1 + prefix: str | None = None @dataclass diff --git a/src/uipath/_cli/_utils/_common.py b/src/uipath/_cli/_utils/_common.py index be49610d5..2ed553128 100644 --- a/src/uipath/_cli/_utils/_common.py +++ b/src/uipath/_cli/_utils/_common.py @@ -1,7 +1,7 @@ import json import os from pathlib import Path -from typing import Literal, Optional +from typing import Literal from urllib.parse import urlparse import click @@ -19,7 +19,7 @@ ) -def get_claim_from_token(claim_name: str) -> Optional[str]: +def get_claim_from_token(claim_name: str) -> str | None: import jwt token = os.getenv(ENV_UIPATH_ACCESS_TOKEN) @@ -59,7 +59,7 @@ def environment_options(function): return function -def get_env_vars(spinner: Optional[Spinner] = None) -> list[str]: +def get_env_vars(spinner: Spinner | None = None) -> list[str]: base_url = os.environ.get("UIPATH_URL") token = os.environ.get("UIPATH_ACCESS_TOKEN") @@ -193,7 +193,7 @@ async def may_override_files( async def read_resource_overwrites_from_file( - directory_path: Optional[str] = None, + directory_path: str | None = None, ) -> dict[str, ResourceOverwrite]: """Read resource overwrites from a JSON file.""" config_file_name = UiPathConfig.config_file_name diff --git a/src/uipath/_cli/_utils/_console.py b/src/uipath/_cli/_utils/_console.py index 130db79b1..69502f6c5 100644 --- a/src/uipath/_cli/_utils/_console.py +++ b/src/uipath/_cli/_utils/_console.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from contextlib import contextmanager from enum import Enum -from typing import Any, Dict, Iterator, List, Optional, Type +from typing import Any, Iterator, Type import click from rich.console import Console @@ -34,9 +36,9 @@ class ConsoleLogger: """A singleton wrapper class for terminal output with emoji support and spinners.""" # Class variable to hold the singleton instance - _instance: Optional["ConsoleLogger"] = None + _instance: ConsoleLogger | None = None - def __new__(cls: Type["ConsoleLogger"]) -> "ConsoleLogger": + def __new__(cls: Type[ConsoleLogger]) -> ConsoleLogger: """Ensure only one instance of ConsoleLogger is created. Returns: @@ -52,10 +54,10 @@ def __init__(self): # Only initialize once if not getattr(self, "_initialized", False): self._console = Console() - self._spinner_live: Optional[Live] = None + self._spinner_live: Live | None = None self._spinner = RichSpinner("dots") - self._progress: Optional[Progress] = None - self._progress_tasks: Dict[str, TaskID] = {} + self._progress: Progress | None = None + self._progress_tasks: dict[str, TaskID] = {} self._initialized = True def _stop_spinner_if_active(self) -> None: @@ -72,7 +74,7 @@ def _stop_progress_if_active(self) -> None: self._progress_tasks.clear() def log( - self, message: str, level: LogLevel = LogLevel.INFO, fg: Optional[str] = None + self, message: str, level: LogLevel = LogLevel.INFO, fg: str | None = None ) -> None: """Log a message with the specified level and optional color. @@ -187,7 +189,7 @@ def confirm( return click.confirm(click.style(message, fg=fg), default=default, **kwargs) def display_options( - self, options: List[Any], message: str = "Select an option:" + self, options: list[Any], message: str = "Select an option:" ) -> None: """Display a list of options with indices. @@ -237,7 +239,7 @@ def update_spinner(self, message: str) -> None: @contextmanager def evaluation_progress( - self, evaluations: List[Dict[str, str]] + self, evaluations: list[dict[str, str]] ) -> Iterator["EvaluationProgressManager"]: """Context manager for evaluation progress tracking. @@ -288,7 +290,7 @@ def get_instance(cls) -> "ConsoleLogger": class EvaluationProgressManager: """Manager for evaluation progress updates.""" - def __init__(self, progress: Progress, tasks: Dict[str, TaskID]): + def __init__(self, progress: Progress, tasks: dict[str, TaskID]): """Initialize the progress manager. Args: diff --git a/src/uipath/_cli/_utils/_eval_set.py b/src/uipath/_cli/_utils/_eval_set.py index 4b6da9fc3..ffb352be0 100644 --- a/src/uipath/_cli/_utils/_eval_set.py +++ b/src/uipath/_cli/_utils/_eval_set.py @@ -1,6 +1,5 @@ import json from pathlib import Path -from typing import List, Optional import click from pydantic import TypeAdapter, ValidationError @@ -67,7 +66,7 @@ def auto_discover_eval_set() -> str: @staticmethod def load_eval_set( - eval_set_path: str, eval_ids: Optional[List[str]] = None + eval_set_path: str, eval_ids: list[str] | None = None ) -> tuple[EvaluationSet, str]: """Load the evaluation set from file. diff --git a/src/uipath/_cli/_utils/_input_args.py b/src/uipath/_cli/_utils/_input_args.py deleted file mode 100644 index 087ba49cf..000000000 --- a/src/uipath/_cli/_utils/_input_args.py +++ /dev/null @@ -1,172 +0,0 @@ -import importlib.util -import inspect -import sys -from dataclasses import fields, is_dataclass -from enum import Enum -from types import ModuleType -from typing import ( - Any, - Dict, - List, - Literal, - Union, - get_args, - get_origin, - get_type_hints, -) - -from pydantic import BaseModel - -SchemaType = Literal["object", "integer", "number", "string", "boolean", "array"] - -TYPE_MAP: Dict[str, SchemaType] = { - "int": "integer", - "float": "number", - "str": "string", - "bool": "boolean", - "list": "array", - "dict": "object", - "List": "array", - "Dict": "object", -} - - -def get_type_schema(type_hint: Any) -> Dict[str, Any]: - """Convert a type hint to a JSON schema.""" - if type_hint is None or type_hint == inspect.Parameter.empty: - return {"type": "object"} - - origin = get_origin(type_hint) - args = get_args(type_hint) - - if origin is Union: - if type(None) in args: - real_type = next(arg for arg in args if arg is not type(None)) - return get_type_schema(real_type) - return {"type": "object"} - - if origin in (list, List): - item_type = args[0] if args else Any - return {"type": "array", "items": get_type_schema(item_type)} - - if origin in (dict, Dict): - return {"type": "object"} - - if inspect.isclass(type_hint): - if issubclass(type_hint, Enum): - enum_values = [member.value for member in type_hint] - if not enum_values: - return {"type": "string", "enum": []} - - first_value = enum_values[0] - if isinstance(first_value, str): - enum_type = "string" - elif isinstance(first_value, int): - enum_type = "integer" - elif isinstance(first_value, float): - enum_type = "number" - elif isinstance(first_value, bool): - enum_type = "boolean" - else: - enum_type = "string" - - return {"type": enum_type, "enum": enum_values} - - if issubclass(type_hint, BaseModel): - properties = {} - required = [] - - # Get the model fields - model_fields = type_hint.model_fields - - for field_name, field_info in model_fields.items(): - # Use alias if defined, otherwise use field name - schema_field_name = field_info.alias if field_info.alias else field_name - - # Get the field type schema - field_schema = get_type_schema(field_info.annotation) - properties[schema_field_name] = field_schema - - # Check if field is required using Pydantic's built-in method - if field_info.is_required(): - required.append(schema_field_name) - - return {"type": "object", "properties": properties, "required": required} - - # Handle dataclasses - elif is_dataclass(type_hint): - properties = {} - required = [] - - for field in fields(type_hint): - field_schema = get_type_schema(field.type) - properties[field.name] = field_schema - if field.default == field.default_factory: - required.append(field.name) - - return {"type": "object", "properties": properties, "required": required} - - # Handle regular classes with annotations - elif hasattr(type_hint, "__annotations__"): - properties = {} - required = [] - - for name, field_type in type_hint.__annotations__.items(): - field_schema = get_type_schema(field_type) - properties[name] = field_schema - # For regular classes, we'll consider all annotated fields as required - # unless they have a default value in __init__ - if hasattr(type_hint, "__init__"): - sig = inspect.signature(type_hint.__init__) - if ( - name in sig.parameters - and sig.parameters[name].default == inspect.Parameter.empty - ): - required.append(name) - else: - required.append(name) - - return {"type": "object", "properties": properties, "required": required} - - type_name = type_hint.__name__ if hasattr(type_hint, "__name__") else str(type_hint) - schema_type = TYPE_MAP.get(type_name, "object") - - return {"type": schema_type} - - -def load_module(file_path: str) -> ModuleType: - """Load a Python module from file path.""" - spec = importlib.util.spec_from_file_location("dynamic_module", file_path) - if not spec or not spec.loader: - raise ImportError(f"Could not load spec for {file_path}") - - module = importlib.util.module_from_spec(spec) - sys.modules["dynamic_module"] = module - spec.loader.exec_module(module) - return module - - -def generate_args(path: str) -> Dict[str, Dict[str, Any]]: - """Generate input/output schema from main function type hints.""" - module = load_module(path) - - main_func = None - for func_name in ["main", "run", "execute"]: - if hasattr(module, func_name): - main_func = getattr(module, func_name) - break - - if not main_func: - raise ValueError("No main function found in module") - - hints = get_type_hints(main_func) - sig = inspect.signature(main_func) - - if not sig.parameters: - return {"input": {}, "output": get_type_schema(hints.get("return", None))} - - input_param_name = next(iter(sig.parameters)) - input_schema = get_type_schema(hints.get(input_param_name)) - output_schema = get_type_schema(hints.get("return")) - - return {"input": input_schema, "output": output_schema} diff --git a/src/uipath/_config.py b/src/uipath/_config.py index c34243c6d..254165806 100644 --- a/src/uipath/_config.py +++ b/src/uipath/_config.py @@ -1,6 +1,5 @@ import os from pathlib import Path -from typing import Optional from pydantic import BaseModel @@ -37,13 +36,13 @@ def config_file_name(self) -> str: return UIPATH_CONFIG_FILE @property - def project_id(self) -> Optional[str]: + def project_id(self) -> str | None: from uipath._utils.constants import ENV_UIPATH_PROJECT_ID return os.getenv(ENV_UIPATH_PROJECT_ID, None) @property - def folder_key(self) -> Optional[str]: + def folder_key(self) -> str | None: from uipath._utils.constants import ENV_FOLDER_KEY return os.getenv(ENV_FOLDER_KEY, None) @@ -53,7 +52,7 @@ def is_studio_project(self) -> bool: return self.project_id is not None @property - def job_key(self) -> Optional[str]: + def job_key(self) -> str | None: from uipath._utils.constants import ENV_JOB_KEY return os.getenv(ENV_JOB_KEY, None) diff --git a/src/uipath/_execution_context.py b/src/uipath/_execution_context.py index 46f4da07f..433934298 100644 --- a/src/uipath/_execution_context.py +++ b/src/uipath/_execution_context.py @@ -1,5 +1,4 @@ from os import environ as env -from typing import Optional from ._utils.constants import ENV_JOB_ID, ENV_JOB_KEY, ENV_ROBOT_KEY @@ -14,24 +13,24 @@ class ExecutionContext: def __init__(self) -> None: try: - self._instance_key: Optional[str] = env[ENV_JOB_KEY] + self._instance_key: str | None = env[ENV_JOB_KEY] except KeyError: self._instance_key = None try: - self._instance_id: Optional[str] = env[ENV_JOB_ID] + self._instance_id: str | None = env[ENV_JOB_ID] except KeyError: self._instance_id = None try: - self._robot_key: Optional[str] = env[ENV_ROBOT_KEY] + self._robot_key: str | None = env[ENV_ROBOT_KEY] except KeyError: self._robot_key = None super().__init__() @property - def instance_id(self) -> Optional[str]: + def instance_id(self) -> str | None: """Get the current job instance ID. The instance ID uniquely identifies the current automation job execution @@ -49,7 +48,7 @@ def instance_id(self) -> Optional[str]: return self._instance_id @property - def instance_key(self) -> Optional[str]: + def instance_key(self) -> str | None: """Get the current job instance key. The instance key uniquely identifies the current automation job execution @@ -61,7 +60,7 @@ def instance_key(self) -> Optional[str]: return self._instance_key @property - def robot_key(self) -> Optional[str]: + def robot_key(self) -> str | None: """Get the current robot key. The robot key identifies the UiPath Robot that is executing the current diff --git a/src/uipath/_folder_context.py b/src/uipath/_folder_context.py index dd8e03ea8..d641007e3 100644 --- a/src/uipath/_folder_context.py +++ b/src/uipath/_folder_context.py @@ -1,5 +1,5 @@ from os import environ as env -from typing import Any, Optional +from typing import Any from ._utils.constants import ( ENV_FOLDER_KEY, @@ -20,12 +20,12 @@ class FolderContext: def __init__(self, **kwargs: Any) -> None: try: - self._folder_key: Optional[str] = env[ENV_FOLDER_KEY] + self._folder_key: str | None = env[ENV_FOLDER_KEY] except KeyError: self._folder_key = None try: - self._folder_path: Optional[str] = env[ENV_FOLDER_PATH] + self._folder_path: str | None = env[ENV_FOLDER_PATH] except KeyError: self._folder_path = None diff --git a/src/uipath/_services/guardrails_service.py b/src/uipath/_services/guardrails_service.py index 7c48351cc..ad91e1881 100644 --- a/src/uipath/_services/guardrails_service.py +++ b/src/uipath/_services/guardrails_service.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Union +from typing import Any from pydantic import BaseModel, ConfigDict, Field @@ -37,11 +37,11 @@ def __init__(self, config: Config, execution_context: ExecutionContext) -> None: @traced("evaluate_guardrail", run_type="uipath") def evaluate_guardrail( self, - input_data: Union[str, Dict[str, Any]], + input_data: str | dict[str, Any], guardrail: Guardrail, *, - folder_key: Optional[str] = None, - folder_path: Optional[str] = None, + folder_key: str | None = None, + folder_path: str | None = None, ) -> BuiltInGuardrailValidationResult: """Call the API to validate input_data with the given guardrail. diff --git a/src/uipath/_services/llm_gateway_service.py b/src/uipath/_services/llm_gateway_service.py index 0a06c072b..62a2d4159 100644 --- a/src/uipath/_services/llm_gateway_service.py +++ b/src/uipath/_services/llm_gateway_service.py @@ -16,7 +16,7 @@ UiPathLlmChatService: Service using UiPath's normalized API format """ -from typing import Any, Dict, List, Optional, Union +from typing import Any from pydantic import BaseModel @@ -77,7 +77,7 @@ class EmbeddingModels(object): text_embedding_ada_002 = "text-embedding-ada-002" -def _cleanup_schema(model_class: type[BaseModel]) -> Dict[str, Any]: +def _cleanup_schema(model_class: type[BaseModel]) -> dict[str, Any]: """Clean up a Pydantic model schema for use with LLM Gateway. This function converts a Pydantic model's JSON schema to a format that's @@ -93,12 +93,11 @@ def _cleanup_schema(model_class: type[BaseModel]) -> Dict[str, Any]: Examples: ```python from pydantic import BaseModel - from typing import List class Country(BaseModel): name: str capital: str - languages: List[str] + languages: list[str] schema = _cleanup_schema(Country) # Returns a clean schema without titles and unnecessary metadata @@ -204,11 +203,11 @@ async def embeddings( @traced(name="llm_chat_completions", run_type="uipath") async def chat_completions( self, - messages: List[Dict[str, str]], + messages: list[dict[str, str]], model: str = ChatModels.gpt_4o_mini_2024_07_18, max_tokens: int = 4096, temperature: float = 0, - response_format: Optional[Union[Dict[str, Any], type[BaseModel]]] = None, + response_format: dict[str, Any] | type[BaseModel] | None = None, api_version: str = API_VERSION, ): """Generate chat completions using UiPath's LLM Gateway service. @@ -265,12 +264,11 @@ async def chat_completions( # Using Pydantic model for structured response from pydantic import BaseModel - from typing import List class Country(BaseModel): name: str capital: str - languages: List[str] + languages: list[str] response = await service.chat_completions( messages=[ @@ -347,18 +345,18 @@ def __init__(self, config: Config, execution_context: ExecutionContext) -> None: @traced(name="llm_chat_completions", run_type="uipath") async def chat_completions( self, - messages: Union[List[Dict[str, str]], List[tuple[str, str]]], + messages: list[dict[str, str]] | list[tuple[str, str]], model: str = ChatModels.gpt_4o_mini_2024_07_18, max_tokens: int = 4096, temperature: float = 0, n: int = 1, frequency_penalty: float = 0, presence_penalty: float = 0, - top_p: Optional[float] = 1, - top_k: Optional[int] = None, - tools: Optional[List[ToolDefinition]] = None, - tool_choice: Optional[ToolChoice] = None, - response_format: Optional[Union[Dict[str, Any], type[BaseModel]]] = None, + top_p: float | None = 1, + top_k: int | None = None, + tools: list[ToolDefinition] | None = None, + tool_choice: ToolChoice | None = None, + response_format: dict[str, Any] | type[BaseModel] | None = None, api_version: str = NORMALIZED_API_VERSION, ): """Generate chat completions using UiPath's normalized LLM Gateway API. @@ -456,12 +454,11 @@ async def chat_completions( # Using Pydantic model for structured response from pydantic import BaseModel - from typing import List class Country(BaseModel): name: str capital: str - languages: List[str] + languages: list[str] response = await service.chat_completions( messages=[ @@ -558,7 +555,7 @@ class Country(BaseModel): return ChatCompletion.model_validate(response.json()) - def _convert_tool_to_uipath_format(self, tool: ToolDefinition) -> Dict[str, Any]: + def _convert_tool_to_uipath_format(self, tool: ToolDefinition) -> dict[str, Any]: """Convert an OpenAI-style tool definition to UiPath API format. This internal method transforms tool definitions from the standard OpenAI format diff --git a/src/uipath/_services/mcp_service.py b/src/uipath/_services/mcp_service.py index a239bb916..e5c490fb4 100644 --- a/src/uipath/_services/mcp_service.py +++ b/src/uipath/_services/mcp_service.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional +from typing import List from .._config import Config from .._execution_context import ExecutionContext @@ -30,7 +30,7 @@ def __init__( def list( self, *, - folder_path: Optional[str] = None, + folder_path: str | None = None, ) -> List[McpServer]: """List all MCP servers. @@ -68,7 +68,7 @@ def list( async def list_async( self, *, - folder_path: Optional[str] = None, + folder_path: str | None = None, ) -> List[McpServer]: """Asynchronously list all MCP servers. @@ -112,7 +112,7 @@ def retrieve( self, slug: str, *, - folder_path: Optional[str] = None, + folder_path: str | None = None, ) -> McpServer: """Retrieve a specific MCP server by its slug. @@ -152,7 +152,7 @@ async def retrieve_async( self, slug: str, *, - folder_path: Optional[str] = None, + folder_path: str | None = None, ) -> McpServer: """Asynchronously retrieve a specific MCP server by its slug. @@ -193,13 +193,13 @@ async def main(): return McpServer.model_validate(response.json()) @property - def custom_headers(self) -> Dict[str, str]: + def custom_headers(self) -> dict[str, str]: return self.folder_headers def _list_spec( self, *, - folder_path: Optional[str], + folder_path: str | None, ) -> RequestSpec: folder_key = self._folders_service.retrieve_folder_key(folder_path) return RequestSpec( @@ -214,7 +214,7 @@ def _retrieve_spec( self, slug: str, *, - folder_path: Optional[str], + folder_path: str | None, ) -> RequestSpec: folder_key = self._folders_service.retrieve_folder_key(folder_path) return RequestSpec( diff --git a/src/uipath/functions/schema_gen.py b/src/uipath/functions/schema_gen.py index bc3f13fe7..74cb85789 100644 --- a/src/uipath/functions/schema_gen.py +++ b/src/uipath/functions/schema_gen.py @@ -3,6 +3,7 @@ import inspect from dataclasses import fields, is_dataclass from enum import Enum +from types import UnionType from typing import Any, Union, get_args, get_origin from pydantic import BaseModel @@ -28,9 +29,11 @@ def get_type_schema(type_hint: Any) -> dict[str, Any]: args = get_args(type_hint) # Handle Optional[T] / Union[T, None] - if origin is Union: + if origin is Union or origin is UnionType: + # Filter out None/NoneType from the union non_none_args = [arg for arg in args if arg is not type(None)] - if non_none_args: + + if len(non_none_args) == 1: return get_type_schema(non_none_args[0]) return {"type": "object"} diff --git a/src/uipath/utils/_endpoints_manager.py b/src/uipath/utils/_endpoints_manager.py index 8ed960811..3b2e22ce0 100644 --- a/src/uipath/utils/_endpoints_manager.py +++ b/src/uipath/utils/_endpoints_manager.py @@ -1,7 +1,6 @@ import logging import os from enum import Enum -from typing import Optional import httpx @@ -63,8 +62,8 @@ class EndpointManager: """ # noqa: D205 _base_url = os.getenv("UIPATH_URL", "") - _agenthub_available: Optional[bool] = None - _orchestrator_available: Optional[bool] = None + _agenthub_available: bool | None = None + _orchestrator_available: bool | None = None @classmethod def is_agenthub_available(cls) -> bool: diff --git a/src/uipath/utils/dynamic_schema.py b/src/uipath/utils/dynamic_schema.py index 9b300d90f..c07f94198 100644 --- a/src/uipath/utils/dynamic_schema.py +++ b/src/uipath/utils/dynamic_schema.py @@ -1,7 +1,7 @@ """Json schema to dynamic pydantic model.""" from enum import Enum -from typing import Any, Dict, List, Type, Union +from typing import Any, Type, Union from pydantic import BaseModel, Field, create_model @@ -37,8 +37,8 @@ def convert_type(prop: dict[str, Any]) -> Any: "number": float, "integer": int, "boolean": bool, - "array": List, - "object": Dict[str, Any], + "array": list, + "object": dict, "null": None, } @@ -58,7 +58,7 @@ class DynamicEnum(base_type, Enum): return type_ elif type_ == "array": item_type: Any = convert_type(prop.get("items", {})) - return List[item_type] # noqa F821 + return list[item_type] # noqa F821 elif type_ == "object": if "properties" in prop: if "title" in prop and prop["title"]: @@ -94,7 +94,7 @@ class DynamicEnum(base_type, Enum): object_model.__doc__ = prop["description"] return object_model else: - return Dict[str, Any] + return dict[str, Any] else: return type_mapping.get(type_, Any) diff --git a/testcases/basic-testcase/main.py b/testcases/basic-testcase/main.py index 0daa80974..49fa0e055 100644 --- a/testcases/basic-testcase/main.py +++ b/testcases/basic-testcase/main.py @@ -1,8 +1,6 @@ import logging from dataclasses import dataclass -from typing import Optional -from uipath.platform import UiPath logger = logging.getLogger(__name__) @@ -10,8 +8,8 @@ @dataclass class EchoIn: message: str - repeat: Optional[int] = 1 - prefix: Optional[str] = None + repeat: int | None = 1 + prefix: str | None = None @dataclass diff --git a/tests/cli/contract/test_sdk_cli_alignment.py b/tests/cli/contract/test_sdk_cli_alignment.py index 251db6533..1298d1e16 100644 --- a/tests/cli/contract/test_sdk_cli_alignment.py +++ b/tests/cli/contract/test_sdk_cli_alignment.py @@ -7,7 +7,7 @@ """ import inspect -from typing import Any, Optional, Set +from typing import Any import click import pytest @@ -23,7 +23,7 @@ SDK_COMMON_PARAMS = {"folder_path", "folder_key"} -def get_cli_option_names(cmd: click.Command) -> Set[str]: +def get_cli_option_names(cmd: click.Command) -> set[str]: """Extract parameter names from a Click command (options AND arguments). Args: @@ -39,7 +39,7 @@ def get_cli_option_names(cmd: click.Command) -> Set[str]: } -def get_sdk_param_names(method) -> Set[str]: +def get_sdk_param_names(method) -> set[str]: """Extract parameter names from SDK method signature. Args: @@ -58,9 +58,9 @@ def assert_cli_sdk_alignment( cli_command: click.Command, sdk_method: Any, *, - exclude_cli: Optional[Set[str]] = None, - exclude_sdk: Optional[Set[str]] = None, - param_mappings: Optional[dict[str, str]] = None, + exclude_cli: set[str] | None = None, + exclude_sdk: set[str] | None = None, + param_mappings: dict[str, str] | None = None, ) -> None: """Assert that CLI command options align with SDK method parameters. diff --git a/tests/cli/mocks/simple_script.py b/tests/cli/mocks/simple_script.py index 1169dac98..eb3265455 100644 --- a/tests/cli/mocks/simple_script.py +++ b/tests/cli/mocks/simple_script.py @@ -1,12 +1,11 @@ from dataclasses import dataclass -from typing import Optional @dataclass class EchoIn: message: str - repeat: Optional[int] = 1 - prefix: Optional[str] = None + repeat: int | None = 1 + prefix: str | None = None @dataclass diff --git a/tests/cli/test_input_args.py b/tests/cli/test_input_args.py index 20d289ff9..f0062275d 100644 --- a/tests/cli/test_input_args.py +++ b/tests/cli/test_input_args.py @@ -5,19 +5,17 @@ from pydantic import BaseModel, Field -from uipath._cli._utils._input_args import get_type_schema +from uipath.functions.schema_gen import get_type_schema class EventArguments(BaseModel): """Test Pydantic model with aliases for testing.""" - event_connector: Optional[str] = Field(default=None, alias="UiPathEventConnector") - event: Optional[str] = Field(default=None, alias="UiPathEvent") - event_object_type: Optional[str] = Field( - default=None, alias="UiPathEventObjectType" - ) - event_object_id: Optional[str] = Field(default=None, alias="UiPathEventObjectId") - additional_event_data: Optional[str] = Field( + event_connector: str | None = Field(default=None, alias="UiPathEventConnector") + event: str | None = Field(default=None, alias="UiPathEvent") + event_object_type: str | None = Field(default=None, alias="UiPathEventObjectType") + event_object_id: str | None = Field(default=None, alias="UiPathEventObjectId") + additional_event_data: str | None = Field( default=None, alias="UiPathAdditionalEventData" ) @@ -26,9 +24,9 @@ class RequiredFieldsModel(BaseModel): """Test Pydantic model with required and optional fields.""" required_field: str - optional_field: Optional[str] = None + optional_field: str | None = None aliased_required: int = Field(alias="AliasedRequired") - aliased_optional: Optional[int] = Field(default=100, alias="AliasedOptional") + aliased_optional: int | None = Field(default=100, alias="AliasedOptional") @dataclass @@ -112,3 +110,9 @@ def test_optional_types(): """Test handling of Optional types.""" schema = get_type_schema(Optional[str]) assert schema == {"type": "string"} # Should unwrap Optional + + +def test_optional_union_types(): + """Test handling of Optional types.""" + schema = get_type_schema(str | None) + assert schema == {"type": "string"} # Should unwrap Optional diff --git a/tests/cli/test_pull.py b/tests/cli/test_pull.py index e7ffec900..7b3aebd1a 100644 --- a/tests/cli/test_pull.py +++ b/tests/cli/test_pull.py @@ -1,7 +1,7 @@ # type: ignore import json import os -from typing import Any, Dict +from typing import Any from unittest.mock import AsyncMock, patch import pytest @@ -42,7 +42,7 @@ def test_successful_pull( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test successful project pull with various file operations.""" @@ -229,7 +229,7 @@ def test_pull_with_existing_files( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, monkeypatch: Any, ) -> None: @@ -310,7 +310,7 @@ def test_pull_with_api_error( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test pull when API request fails.""" @@ -339,7 +339,7 @@ def test_pull_non_coded_agent_project( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test pull when the project is not a coded agent project (missing pyproject.toml).""" @@ -397,7 +397,7 @@ def test_pull_multiple_eval_folders( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test that pull command uses evaluations folder instead of evals.""" diff --git a/tests/cli/test_push.py b/tests/cli/test_push.py index 60c27ec9a..9708b3725 100644 --- a/tests/cli/test_push.py +++ b/tests/cli/test_push.py @@ -2,7 +2,7 @@ import json import os import re -from typing import Any, Dict, Optional +from typing import Any from unittest.mock import AsyncMock, patch import pytest @@ -115,7 +115,7 @@ def _mock_file_download( httpx_mock, file_id: str, *, - file_content: Optional[str] = None, + file_content: str | None = None, times: int = 1, ): for _ in range(times): @@ -139,7 +139,7 @@ def test_push_without_uipath_json( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], ) -> None: """Test push when uipath.json is missing.""" with runner.isolated_filesystem(temp_dir=temp_dir): @@ -160,7 +160,7 @@ def test_push_without_required_files_shows_specific_missing( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], ) -> None: """Test push shows specific missing files when uipath.json and .uipath are missing.""" with runner.isolated_filesystem(temp_dir=temp_dir): @@ -186,7 +186,7 @@ def test_push_with_only_enty_points_json_missing( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], ) -> None: """Test push when .uipath directory exists but uipath.json is missing.""" with runner.isolated_filesystem(temp_dir=temp_dir): @@ -209,7 +209,7 @@ def test_push_without_project_id( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], ) -> None: """Test push when UIPATH_PROJECT_ID is missing.""" with runner.isolated_filesystem(temp_dir=temp_dir): @@ -228,7 +228,7 @@ def test_successful_push( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test successful project push with various file operations.""" @@ -389,7 +389,7 @@ def test_successful_push_new_project( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test successful project push with various file operations.""" @@ -502,7 +502,7 @@ def test_push_with_api_error( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test push when API request fails.""" @@ -538,7 +538,7 @@ def test_push_non_coded_agent_project( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test push when the project is not a coded agent project (missing pyproject.toml).""" @@ -600,7 +600,7 @@ def test_push_with_nolock_flag( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test push command with --nolock flag.""" @@ -703,7 +703,7 @@ def test_push_files_excluded( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test that files mentioned in filesExcluded are excluded from push.""" @@ -782,7 +782,7 @@ def test_push_files_excluded_takes_precedence_over_included( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test that filesExcluded takes precedence over filesIncluded in push.""" @@ -860,7 +860,7 @@ def test_push_filename_vs_path_exclusion( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test that filename exclusion only affects root directory, path exclusion affects specific paths in push.""" @@ -950,7 +950,7 @@ def test_push_filename_vs_path_inclusion( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test that filename inclusion only affects root directory, path inclusion affects specific paths in push.""" @@ -1056,7 +1056,7 @@ def test_push_directory_name_vs_path_exclusion( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test that directory exclusion by name only affects root level, by path affects specific paths in push.""" @@ -1161,7 +1161,7 @@ def test_push_detects_source_file_conflicts( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, monkeypatch: Any, ) -> None: @@ -1238,7 +1238,7 @@ def test_push_shows_up_to_date_for_unchanged_files( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test that push shows 'up to date' message for files that haven't changed.""" @@ -1333,7 +1333,7 @@ def test_push_preserves_remote_evals_when_no_local_evals( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test that remote evaluations files are not deleted when no local evals folder exists.""" @@ -1475,7 +1475,7 @@ def _mock_file_download( httpx_mock, file_id: str, *, - file_content: Optional[str] = None, + file_content: str | None = None, times: int = 1, ): for _ in range(times): @@ -1491,7 +1491,7 @@ def test_push_with_resources_imports_referenced_resources( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test that push without --ignore-resources flag imports referenced resources to the solution.""" @@ -1679,7 +1679,7 @@ def test_push_with_ignore_resources_flag_skips_resource_import( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test that push with --ignore-resources flag skips resource import.""" @@ -1791,7 +1791,7 @@ def test_push_with_resource_not_found_shows_warning( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test that push shows warning when referenced resource is not found in catalog.""" @@ -1922,7 +1922,7 @@ def test_push_with_resource_already_exists_shows_unchanged( runner: CliRunner, temp_dir: str, project_details: ProjectDetails, - mock_env_vars: Dict[str, str], + mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: """Test that push shows unchanged message when referenced resource already exists.""" diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index a5438293f..2426fa19e 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -266,20 +266,19 @@ def test_pydantic_model_execution( ): """Test successful execution with Pydantic models.""" pydantic_script = """ -from typing import Optional from pydantic import BaseModel, Field class PersonIn(BaseModel): name: str age: int - email: Optional[str] = None + email: str | None = None class PersonOut(BaseModel): name: str age: int - email: Optional[str] = None + email: str | None = None is_adult: bool greeting: str diff --git a/tests/cli/utils/common.py b/tests/cli/utils/common.py index cd1fcd920..b9b184fb0 100644 --- a/tests/cli/utils/common.py +++ b/tests/cli/utils/common.py @@ -1,7 +1,6 @@ import os -from typing import Dict -def configure_env_vars(env_vars: Dict[str, str]): +def configure_env_vars(env_vars: dict[str, str]): os.environ.clear() os.environ.update(env_vars) diff --git a/tests/cli/utils/test_dynamic_schema.py b/tests/cli/utils/test_dynamic_schema.py index a0f101b6c..13d86c146 100644 --- a/tests/cli/utils/test_dynamic_schema.py +++ b/tests/cli/utils/test_dynamic_schema.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import List, Optional from pydantic import BaseModel, Field @@ -11,7 +10,7 @@ def atest_dynamic_schema(): class InnerSchema(BaseModel): """Inner schema description including a self-reference.""" - self_reference: Optional["InnerSchema"] = None + self_reference: "InnerSchema" | None = None class CustomEnum(str, Enum): KEY_1 = "VALUE_1" @@ -23,24 +22,24 @@ class Schema(BaseModel): string: str = Field( default="", title="String Title", description="String Description" ) - optional_string: Optional[str] = Field( + optional_string: str | None = Field( default=None, title="Optional String Title", description="Optional String Description", ) - list_str: List[str] = Field( + list_str: list[str] = Field( default=[], title="List String", description="List String Description" ) integer: int = Field( default=0, title="Integer Title", description="Integer Description" ) - optional_integer: Optional[int] = Field( + optional_integer: int | None = Field( default=None, title="Option Integer Title", description="Option Integer Description", ) - list_integer: List[int] = Field( + list_integer: list[int] = Field( default=[], title="List Integer Title", description="List Integer Description", @@ -49,12 +48,12 @@ class Schema(BaseModel): floating: float = Field( default=0.0, title="Floating Title", description="Floating Description" ) - optional_floating: Optional[float] = Field( + optional_floating: float | None = Field( default=None, title="Option Floating Title", description="Option Floating Description", ) - list_floating: List[float] = Field( + list_floating: list[float] = Field( default=[], title="List Floating Title", description="List Floating Description", @@ -63,12 +62,12 @@ class Schema(BaseModel): boolean: bool = Field( default=False, title="Boolean Title", description="Boolean Description" ) - optional_boolean: Optional[bool] = Field( + optional_boolean: bool | None = Field( default=None, title="Option Boolean Title", description="Option Boolean Description", ) - list_boolean: List[bool] = Field( + list_boolean: list[bool] = Field( default=[], title="List Boolean Title", description="List Boolean Description", @@ -79,12 +78,12 @@ class Schema(BaseModel): title="Nested Object Title", description="Nested Object Description", ) - optional_nested_object: Optional[InnerSchema] = Field( + optional_nested_object: InnerSchema | None = Field( default=None, title="Optional Nested Object Title", description="Optional Nested Object Description", ) - list_nested_object: List[InnerSchema] = Field( + list_nested_object: list[InnerSchema] = Field( default=[], title="List Nested Object Title", description="List Nested Object Description", diff --git a/tests/sdk/services/test_attachments_service.py b/tests/sdk/services/test_attachments_service.py index df7590058..e33dc1bca 100644 --- a/tests/sdk/services/test_attachments_service.py +++ b/tests/sdk/services/test_attachments_service.py @@ -2,7 +2,7 @@ import os import shutil import uuid -from typing import TYPE_CHECKING, Any, Dict, Generator, Tuple +from typing import TYPE_CHECKING, Any, Generator, Tuple import pytest from pytest_httpx import HTTPXMock @@ -107,7 +107,7 @@ def local_attachment_file( @pytest.fixture -def blob_uri_response() -> Dict[str, Any]: +def blob_uri_response() -> dict[str, Any]: """Provides a mock response for blob access requests. Returns: @@ -139,7 +139,7 @@ def test_upload_with_file_path( tenant: str, version: str, temp_file: Tuple[str, str, str], - blob_uri_response: Dict[str, Any], + blob_uri_response: dict[str, Any], ) -> None: """Test uploading an attachment from a file path. @@ -215,7 +215,7 @@ def test_upload_with_content( org: str, tenant: str, version: str, - blob_uri_response: Dict[str, Any], + blob_uri_response: dict[str, Any], ) -> None: """Test uploading an attachment with in-memory content. @@ -310,7 +310,7 @@ async def test_upload_async_with_content( base_url: str, org: str, tenant: str, - blob_uri_response: Dict[str, Any], + blob_uri_response: dict[str, Any], ) -> None: """Test asynchronously uploading an attachment with in-memory content. @@ -376,7 +376,7 @@ def test_download( tenant: str, version: str, tmp_path: Any, - blob_uri_response: Dict[str, Any], + blob_uri_response: dict[str, Any], ) -> None: """Test downloading an attachment. @@ -456,7 +456,7 @@ async def test_download_async( tenant: str, version: str, tmp_path: Any, - blob_uri_response: Dict[str, Any], + blob_uri_response: dict[str, Any], ) -> None: """Test asynchronously downloading an attachment. diff --git a/tests/sdk/services/test_entities_service.py b/tests/sdk/services/test_entities_service.py index e578558ff..8148a7ebd 100644 --- a/tests/sdk/services/test_entities_service.py +++ b/tests/sdk/services/test_entities_service.py @@ -7,7 +7,7 @@ from uipath._config import Config from uipath._execution_context import ExecutionContext -from uipath._services import EntitiesService +from uipath._services.entities_service import EntitiesService from uipath.platform.entities import Entity @@ -36,7 +36,7 @@ def record_schema(request): @pytest.fixture(params=[True, False], ids=["optional_field", "required_field"]) def record_schema_optional(request): is_optional = request.param - field_type = Optional[int] if is_optional else int + field_type = Optional[int] | None if is_optional else int schema_name = f"RecordSchema{'Optional' if is_optional else 'Required'}" RecordSchemaOptional = make_dataclass( diff --git a/tests/sdk/services/test_llm_schema_cleanup.py b/tests/sdk/services/test_llm_schema_cleanup.py index 4c2da742d..ead17272f 100644 --- a/tests/sdk/services/test_llm_schema_cleanup.py +++ b/tests/sdk/services/test_llm_schema_cleanup.py @@ -1,7 +1,5 @@ """Tests for the _cleanup_schema function in LLM Gateway Service.""" -from typing import List, Optional - from pydantic import BaseModel from uipath._services.llm_gateway_service import _cleanup_schema @@ -15,13 +13,13 @@ class SimpleModel(BaseModel): class ModelWithList(BaseModel): - names: List[str] - numbers: List[int] + names: list[str] + numbers: list[int] class ModelWithOptional(BaseModel): required_field: str - optional_field: Optional[str] = None + optional_field: str | None = None # Complex nested models for comprehensive testing @@ -34,26 +32,26 @@ class Task(BaseModel): class Project(BaseModel): project_id: int name: str - tasks: List[Task] + tasks: list[Task] class Team(BaseModel): team_id: int team_name: str - members: List[str] - projects: List[Project] + members: list[str] + projects: list[Project] class Department(BaseModel): department_id: int department_name: str - teams: List[Team] + teams: list[Team] class Company(BaseModel): company_id: int company_name: str - departments: List[Department] + departments: list[Department] class TestCleanupSchema: @@ -212,7 +210,7 @@ class BaseEntity(BaseModel): class ExtendedEntity(BaseEntity): name: str - description: Optional[str] = None + description: str | None = None schema = _cleanup_schema(ExtendedEntity) diff --git a/tests/sdk/services/test_llm_service.py b/tests/sdk/services/test_llm_service.py index cdfd0ae3f..6710ccb3f 100644 --- a/tests/sdk/services/test_llm_service.py +++ b/tests/sdk/services/test_llm_service.py @@ -1,5 +1,4 @@ import json -from typing import List, Optional from unittest.mock import MagicMock, patch import pytest @@ -104,23 +103,23 @@ class Task(BaseModel): class Project(BaseModel): project_id: int name: str - tasks: List[Task] + tasks: list[Task] class Team(BaseModel): team_id: int team_name: str - members: List[str] - projects: List[Project] + members: list[str] + projects: list[Project] class Department(BaseModel): department_id: int department_name: str - teams: List[Team] + teams: list[Team] class Company(BaseModel): company_id: int company_name: str - departments: List[Department] + departments: list[Department] # Mock response mock_response = MagicMock() @@ -294,7 +293,7 @@ async def test_optional_request_format_model(self, mock_request, llm_service): """Test using complex Company Pydantic model as response_format.""" class Article(BaseModel): - title: Optional[str] = None + title: str | None = None # Mock response mock_response = MagicMock() diff --git a/uv.lock b/uv.lock index b5214805b..d1ff7d291 100644 --- a/uv.lock +++ b/uv.lock @@ -1893,14 +1893,14 @@ wheels = [ [[package]] name = "pydantic-function-models" -version = "0.1.10" +version = "0.1.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/83/dc9cf4c16159266e643a16b14dd90c24e859670fbe2611140c0cd5503cae/pydantic_function_models-0.1.10.tar.gz", hash = "sha256:d88e37c19bc2b9d88850a6f00f0227212aae1b0d55de45c9de7af65373844027", size = 9150, upload-time = "2025-02-17T16:53:34.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/9f/9f89602abf782693974d1812657b7b0bab346c011f31d7a05ca71f5643e2/pydantic_function_models-0.1.11.tar.gz", hash = "sha256:28292961bc71f9e4d75ae608ef1cf820ce650ba019067776ee82c6612ccf1cca", size = 9018, upload-time = "2025-11-29T20:16:39.726Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/c6/c8412c88f4113b7cf3b33ae08f4abcd94fe0413d09ec49780f35f8f9e790/pydantic_function_models-0.1.10-py3-none-any.whl", hash = "sha256:9c1b0be9537a48f3ad9e3d9dd6c4e9ebcce98dd79a1bb329868b576cf50452c1", size = 8061, upload-time = "2025-02-17T16:53:28.904Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5c/cd8bd73e893604e335f4347539851a8f8dede32fbf6d7a009f564c2c6681/pydantic_function_models-0.1.11-py3-none-any.whl", hash = "sha256:1c17d45f7b7b95ad1644226a9b8d6d05ce1565a0d0bbe03f4ec86e21487aff2b", size = 8028, upload-time = "2025-11-29T20:16:38.391Z" }, ] [[package]] @@ -2401,7 +2401,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.2.7" +version = "2.2.8" source = { editable = "." } dependencies = [ { name = "click" }, @@ -2458,7 +2458,7 @@ requires-dist = [ { name = "mermaid-builder", specifier = "==0.0.3" }, { name = "mockito", specifier = ">=1.5.4" }, { name = "pathlib", specifier = ">=1.0.1" }, - { name = "pydantic-function-models", specifier = ">=0.1.10" }, + { name = "pydantic-function-models", specifier = ">=0.1.11" }, { name = "pyjwt", specifier = ">=2.10.1" }, { name = "pysignalr", specifier = "==1.3.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, @@ -2466,7 +2466,7 @@ requires-dist = [ { name = "tenacity", specifier = ">=9.0.0" }, { name = "truststore", specifier = ">=0.10.1" }, { name = "uipath-core", specifier = ">=0.0.5,<0.1.0" }, - { name = "uipath-runtime", specifier = ">=0.1.0,<0.2.0" }, + { name = "uipath-runtime", specifier = ">=0.1.1,<0.2.0" }, ] [package.metadata.requires-dev] @@ -2512,14 +2512,14 @@ wheels = [ [[package]] name = "uipath-runtime" -version = "0.1.0" +version = "0.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "uipath-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/5f/b48eaa87501ccffb067878ff99773fa89fbc1cafa8ab736d8fd5ae099b59/uipath_runtime-0.1.0.tar.gz", hash = "sha256:2a262eb29faeb1d62158ccaf1d1ec44752813625fdbcab5671a625c999c433ac", size = 87980, upload-time = "2025-11-29T13:10:32.269Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/e9/7b2549a15681fa6a874193690627ad8b849a0bf72f4ce51fb0fa07cfb301/uipath_runtime-0.1.1.tar.gz", hash = "sha256:a2ccb930a3b18ef8e7063e5a943cf534c0815d0e019743a26cef5cbacfa2c05b", size = 87996, upload-time = "2025-11-30T16:40:19.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/2a/39364e985269ac27b6b78d0323e6f516e25287bca87938a2088442b5cf06/uipath_runtime-0.1.0-py3-none-any.whl", hash = "sha256:997b53737fc6f22bb2e80700fd45c6b0a7912af6843fd4a103c58bdcf30f7fdd", size = 34207, upload-time = "2025-11-29T13:10:31.127Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c3/4a22b7716b6778cede22f83d3eb49fdd8b4031811e953ff5d8240ecd4bd5/uipath_runtime-0.1.1-py3-none-any.whl", hash = "sha256:c0a21dfc5d4467244862666528c052ec5ac2488df0a7aa30b43129a3140d5e36", size = 34228, upload-time = "2025-11-30T16:40:18.191Z" }, ] [[package]]