diff --git a/.github/workflows/lint-packages.yml b/.github/workflows/lint-packages.yml index d530e25a9..94367f552 100644 --- a/.github/workflows/lint-packages.yml +++ b/.github/workflows/lint-packages.yml @@ -139,6 +139,11 @@ jobs: working-directory: packages/uipath-platform run: uv run ruff format --check . + - name: Check import contracts + if: steps.check.outputs.skip != 'true' + working-directory: packages/uipath-platform + run: uv run lint-imports + lint-uipath: name: Lint uipath needs: detect-changed-packages diff --git a/.gitignore b/.gitignore index 6b24bbd06..fd06e77db 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ wheels/ .cache/* .cache +.import_linter_cache/ site diff --git a/packages/uipath-platform/CLAUDE.md b/packages/uipath-platform/CLAUDE.md index 21896676f..a13260c41 100644 --- a/packages/uipath-platform/CLAUDE.md +++ b/packages/uipath-platform/CLAUDE.md @@ -17,6 +17,7 @@ pytest tests/services/test_assets_service.py # Single test file ruff check . # Lint ruff format --check . # Format check mypy src tests # Type check +uv run lint-imports # Import-graph layering contract ``` No justfile exists for this package — run commands directly. diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 42178758a..bae8dc441 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.9" +version = "0.2.10" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" @@ -47,6 +47,7 @@ dev = [ "pytest-cov>=4.1.0", "pytest-mock>=3.11.1", "pre-commit>=4.1.0", + "import-linter>=2.0", ] [tool.hatch.build.targets.wheel] @@ -126,6 +127,30 @@ exclude_lines = [ "@(abc\\.)?abstractmethod", ] +[tool.importlinter] +root_package = "uipath.platform" +exclude_type_checking_imports = true + +[[tool.importlinter.contracts]] +id = "platform-layers" +name = "uipath-platform layered architecture" +type = "layers" +containers = ["uipath.platform"] +exhaustive = true +# TODO: Remove this exception in the next breaking-change release. +ignore_imports = [ + "uipath.platform.common.interrupt_models -> uipath.platform.resume_triggers.interrupt_models", +] +unmatched_ignore_imports_alerting = "error" +layers = [ + "_uipath : resume_triggers", + "agenthub | automation_ops | automation_tracker | connections | context_grounding | entities | external_applications | governance | guardrails | memory | pii_detection | portal | resource_catalog | semantic_proxy", + "orchestrator", + "action_center | attachments | chat | documents | identity", + "common", + "constants | errors", +] + [tool.uv] exclude-newer = "2 days" diff --git a/packages/uipath-platform/src/uipath/platform/_uipath.py b/packages/uipath-platform/src/uipath/platform/_uipath.py index 98af7b8b6..f2359cd6f 100644 --- a/packages/uipath-platform/src/uipath/platform/_uipath.py +++ b/packages/uipath-platform/src/uipath/platform/_uipath.py @@ -1,8 +1,6 @@ from functools import cached_property from typing import Optional -from pydantic import ValidationError - from uipath.platform.automation_tracker import AutomationTrackerService from .action_center import TasksService @@ -12,15 +10,13 @@ from .chat import ConversationsService, UiPathLlmChatService, UiPathOpenAIService from .common import ( ApiClient, - UiPathApiConfig, UiPathExecutionContext, ) -from .common.auth import resolve_config_from_env +from .common.auth import build_api_config, resolve_config_from_env from .connections import ConnectionsService from .context_grounding import ContextGroundingService from .documents import DocumentsService from .entities import EntitiesService -from .errors import BaseUrlMissingError, SecretMissingError from .external_applications import ExternalApplicationService from .governance import GovernanceService from .guardrails import GuardrailsService @@ -60,25 +56,15 @@ def __init__( scope: Optional[str] = None, debug: bool = False, ) -> None: - try: - if _has_valid_client_credentials(client_id, client_secret): - assert client_id and client_secret - service = ExternalApplicationService(base_url) - token_data = service.get_token_data(client_id, client_secret, scope) - base_url, secret = service._base_url, token_data.access_token - else: - base_url, secret = resolve_config_from_env(base_url, secret) - - self._config = UiPathApiConfig( - base_url=base_url, - secret=secret, - ) - except ValidationError as e: - for error in e.errors(): - if error["loc"][0] == "base_url": - raise BaseUrlMissingError() from e - elif error["loc"][0] == "secret": - raise SecretMissingError() from e + if _has_valid_client_credentials(client_id, client_secret): + assert client_id and client_secret + service = ExternalApplicationService(base_url) + token_data = service.get_token_data(client_id, client_secret, scope) + base_url, secret = service._base_url, token_data.access_token + else: + base_url, secret = resolve_config_from_env(base_url, secret) + + self._config = build_api_config(base_url, secret) self._execution_context = UiPathExecutionContext() @property diff --git a/packages/uipath-platform/src/uipath/platform/common/__init__.py b/packages/uipath-platform/src/uipath/platform/common/__init__.py index 802ec67bc..d322c1aea 100644 --- a/packages/uipath-platform/src/uipath/platform/common/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/common/__init__.py @@ -3,8 +3,41 @@ This module contains common models used across multiple services. """ +from typing import TYPE_CHECKING, Any + from uipath.core.triggers import UiPathResumeMetadata +# TODO: Remove the interrupt-model compatibility exports in the next breaking-change release. +if TYPE_CHECKING: + from .interrupt_models import ( + CreateBatchTransform, + CreateDeepRag, + CreateDeepRagRaw, + CreateEphemeralIndex, + CreateEphemeralIndexRaw, + CreateEscalation, + CreateTask, + DocumentExtraction, + DocumentExtractionValidation, + InvokeProcess, + InvokeProcessRaw, + InvokeSystemAgent, + WaitBatchTransform, + WaitDeepRag, + WaitDeepRagRaw, + WaitDocumentExtraction, + WaitDocumentExtractionValidation, + WaitEphemeralIndex, + WaitEphemeralIndexRaw, + WaitEscalation, + WaitIntegrationEvent, + WaitJob, + WaitJobRaw, + WaitSystemAgent, + WaitTask, + WaitUntil, + ) + from ._api_client import ApiClient from ._base_service import BaseService, resolve_trace_id from ._bindings import ( @@ -41,34 +74,6 @@ from ._user_agent import user_agent_value from .auth import TokenData from .dynamic_schema import jsonschema_to_pydantic -from .interrupt_models import ( - CreateBatchTransform, - CreateDeepRag, - CreateDeepRagRaw, - CreateEphemeralIndex, - CreateEphemeralIndexRaw, - CreateEscalation, - CreateTask, - DocumentExtraction, - DocumentExtractionValidation, - InvokeProcess, - InvokeProcessRaw, - InvokeSystemAgent, - WaitBatchTransform, - WaitDeepRag, - WaitDeepRagRaw, - WaitDocumentExtraction, - WaitDocumentExtractionValidation, - WaitEphemeralIndex, - WaitEphemeralIndexRaw, - WaitEscalation, - WaitIntegrationEvent, - WaitJob, - WaitJobRaw, - WaitSystemAgent, - WaitTask, - WaitUntil, -) from .paging import PagedResult from .timeout import ( UiPathTimeoutError, @@ -151,3 +156,49 @@ ] from .validation import validate_pagination_params + +_INTERRUPT_MODEL_NAMES = { + "CreateBatchTransform", + "CreateDeepRag", + "CreateDeepRagRaw", + "CreateEphemeralIndex", + "CreateEphemeralIndexRaw", + "CreateEscalation", + "CreateTask", + "DocumentExtraction", + "DocumentExtractionValidation", + "InvokeProcess", + "InvokeProcessRaw", + "InvokeSystemAgent", + "WaitBatchTransform", + "WaitDeepRag", + "WaitDeepRagRaw", + "WaitDocumentExtraction", + "WaitDocumentExtractionValidation", + "WaitEphemeralIndex", + "WaitEphemeralIndexRaw", + "WaitEscalation", + "WaitIntegrationEvent", + "WaitJob", + "WaitJobRaw", + "WaitSystemAgent", + "WaitTask", + "WaitUntil", +} + + +def __getattr__(name: str) -> Any: + """Resolve moved interrupt models on demand.""" + if name not in _INTERRUPT_MODEL_NAMES: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + from . import interrupt_models + + value = interrupt_models._resolve(name) + globals()[name] = value + return value + + +def __dir__() -> list[str]: + """List common exports, including compatibility aliases.""" + return sorted(set(globals()) | set(__all__)) diff --git a/packages/uipath-platform/src/uipath/platform/common/auth.py b/packages/uipath-platform/src/uipath/platform/common/auth.py index a8f87d3e4..612d3ca4f 100644 --- a/packages/uipath-platform/src/uipath/platform/common/auth.py +++ b/packages/uipath-platform/src/uipath/platform/common/auth.py @@ -3,13 +3,16 @@ from os import environ as env from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError from uipath.platform.constants import ( ENV_BASE_URL, ENV_UIPATH_ACCESS_TOKEN, ENV_UNATTENDED_USER_ACCESS_TOKEN, ) +from uipath.platform.errors import BaseUrlMissingError, SecretMissingError + +from ._config import UiPathApiConfig class TokenData(BaseModel): @@ -35,3 +38,24 @@ def resolve_config_from_env( or env.get(ENV_UIPATH_ACCESS_TOKEN) ) return base_url_value, secret_value + + +def build_api_config( + base_url: Optional[str], + secret: Optional[str], +) -> UiPathApiConfig: + """Build a validated API config, translating missing fields into typed errors. + + Raises: + BaseUrlMissingError: If ``base_url`` is missing. + SecretMissingError: If ``secret`` is missing. + """ + try: + return UiPathApiConfig(base_url=base_url, secret=secret) # type: ignore[arg-type] + except ValidationError as e: + for error in e.errors(): + if error["loc"][0] == "base_url": + raise BaseUrlMissingError() from e + elif error["loc"][0] == "secret": + raise SecretMissingError() from e + raise diff --git a/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py b/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py index ee9104c54..e47406417 100644 --- a/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py +++ b/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py @@ -1,298 +1,96 @@ -"""Models for interrupt operations in UiPath platform.""" - -from datetime import datetime, timezone -from typing import Annotated, Any - -from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator - -from uipath.platform.context_grounding.context_grounding_index import ( - ContextGroundingIndex, -) - -from ..action_center.tasks import Task, TaskRecipient -from ..attachments import Attachment -from ..context_grounding import ( - BatchTransformCreationResponse, - BatchTransformOutputColumn, - CitationMode, - DeepRagCreationResponse, - EphemeralIndexUsage, -) -from ..documents import ( - ActionPriority, - ExtractionResponseIXP, - FileContent, - StartExtractionResponse, -) -from ..documents.documents import StartExtractionValidationResponse -from ..orchestrator.job import Job - - -class InvokeProcess(BaseModel): - """Model representing a process invocation.""" - - name: str - process_folder_path: str | None = None - process_folder_key: str | None = None - input_arguments: dict[str, Any] | None - attachments: list[Attachment] | None = None - - -class WaitJob(BaseModel): - """Model representing a wait job operation.""" - - job: Job - process_folder_path: str | None = None - process_folder_key: str | None = None - - -class InvokeProcessRaw(InvokeProcess): - """Model representing a raw process invocation (returns job without state validation).""" - - pass - - -class WaitJobRaw(WaitJob): - """Model representing a raw wait job operation (returns job without state validation).""" - - pass - - -class CreateTask(BaseModel): - """Model representing an action creation.""" - - title: str - data: dict[str, Any] | None = None - assignee: str | None = "" - recipient: TaskRecipient | None = None - app_name: str | None = None - app_folder_path: str | None = None - app_folder_key: str | None = None - app_key: str | None = None - priority: str | None = None - labels: list[str] | None = None - is_actionable_message_enabled: bool | None = None - actionable_message_metadata: dict[str, Any] | None = None - source_name: str = "Agent" - - -class CreateEscalation(CreateTask): - """Model representing an escalation creation.""" - - pass - - -class WaitTask(BaseModel): - """Model representing a wait action operation.""" - - action: Task - app_folder_path: str | None = None - app_folder_key: str | None = None - app_name: str | None = None - recipient: TaskRecipient | None = None - - -class WaitEscalation(WaitTask): - """Model representing a wait escalation operation.""" - - pass - - -class CreateDeepRag(BaseModel): - """Model representing a Deep RAG task creation.""" - - name: str - index_name: Annotated[str, Field(max_length=512)] | None = None - index_id: Annotated[str, Field(max_length=512)] | None = None - prompt: Annotated[str, Field(max_length=250000)] - glob_pattern: Annotated[str, Field(max_length=512, default="*")] = "**" - citation_mode: CitationMode = CitationMode.SKIP - index_folder_key: str | None = None - index_folder_path: str | None = None - is_ephemeral_index: bool | None = None - - @model_validator(mode="after") - def validate_ephemeral_index_requires_index_id(self) -> "CreateDeepRag": - """Validate that if it is an ephemeral index that it is using index id.""" - if self.is_ephemeral_index is True and self.index_id is None: - raise ValueError("Index id must be provided for an ephemeral index") - return self - - -class CreateDeepRagRaw(CreateDeepRag): - """Model representing a Deep RAG task creation (returns the deep_rag without status validation).""" - - pass - - -class WaitDeepRag(BaseModel): - """Model representing a wait Deep RAG task.""" - - deep_rag: DeepRagCreationResponse - index_folder_path: str | None = None - index_folder_key: str | None = None - - -class WaitDeepRagRaw(WaitDeepRag): - """Model representing a wait Deep RAG task (returns the deep_rag without status validation).""" - - pass - - -class CreateEphemeralIndex(BaseModel): - """Model representing an Ephemeral Index task creation.""" - - usage: EphemeralIndexUsage - attachments: list[str] - - -class CreateEphemeralIndexRaw(CreateEphemeralIndex): - """Model representing an Ephemeral Index task creation (returns the ephemeral index without status validation).""" - - pass - - -class WaitEphemeralIndex(BaseModel): - """Model representing a wait Ephemeral Index task.""" - - index: ContextGroundingIndex - - -class WaitEphemeralIndexRaw(WaitEphemeralIndex): - """Model representing a wait Ephemeral Index task (returns the ephemeral index without status validation).""" - - pass - - -class CreateBatchTransform(BaseModel): - """Model representing a Batch Transform task creation.""" - - name: str - index_name: str | None = None - index_id: Annotated[str, Field(max_length=512)] | None = None - prompt: Annotated[str, Field(max_length=250000)] - output_columns: list[BatchTransformOutputColumn] - storage_bucket_folder_path_prefix: Annotated[str | None, Field(max_length=512)] = ( - None +"""Deprecated lazy aliases for resume trigger interrupt models.""" + +import warnings +from typing import TYPE_CHECKING, Any + +# TODO: Remove this compatibility module in the next breaking-change release. +if TYPE_CHECKING: + from uipath.platform.resume_triggers.interrupt_models import ( + CreateBatchTransform, + CreateDeepRag, + CreateDeepRagRaw, + CreateEphemeralIndex, + CreateEphemeralIndexRaw, + CreateEscalation, + CreateTask, + DocumentExtraction, + DocumentExtractionValidation, + InvokeProcess, + InvokeProcessRaw, + InvokeSystemAgent, + WaitBatchTransform, + WaitDeepRag, + WaitDeepRagRaw, + WaitDocumentExtraction, + WaitDocumentExtractionValidation, + WaitEphemeralIndex, + WaitEphemeralIndexRaw, + WaitEscalation, + WaitIntegrationEvent, + WaitJob, + WaitJobRaw, + WaitSystemAgent, + WaitTask, + WaitUntil, ) - enable_web_search_grounding: bool = False - destination_path: str - index_folder_key: str | None = None - index_folder_path: str | None = None - is_ephemeral_index: bool | None = None - - @model_validator(mode="after") - def validate_ephemeral_index_requires_index_id(self) -> "CreateBatchTransform": - """Validate that if it is an ephemeral index that it is using index id.""" - if self.is_ephemeral_index is True and self.index_id is None: - raise ValueError("Index id must be provided for an ephemeral index") - return self - - -class WaitBatchTransform(BaseModel): - """Model representing a wait Batch Transform task.""" - - batch_transform: BatchTransformCreationResponse - index_folder_path: str | None = None - index_folder_key: str | None = None - - -class InvokeSystemAgent(BaseModel): - """Model representing a system agent job invocation.""" - - agent_name: str - entrypoint: str - input_arguments: dict[str, Any] | None = None - folder_path: str | None = None - folder_key: str | None = None - - -class WaitSystemAgent(BaseModel): - """Model representing a wait system agent job invocation.""" - job_key: str - process_folder_path: str | None = None - process_folder_key: str | None = None - - -class DocumentExtraction(BaseModel): - """Model representing a document extraction task creation.""" - - project_name: str - tag: str - file: FileContent | None = None - file_path: str | None = None - - model_config = ConfigDict( - arbitrary_types_allowed=True, - ) - - @model_validator(mode="after") - def validate_exactly_one_file_source(self) -> "DocumentExtraction": - """Validate that exactly one of file or file_path is provided.""" - if (self.file is None) == (self.file_path is None): - raise ValueError( - "Exactly one of 'file' or 'file_path' must be provided, not both or neither" - ) - return self - - -class WaitDocumentExtraction(BaseModel): - """Model representing a wait document extraction task creation.""" - - extraction: StartExtractionResponse - - -class DocumentExtractionValidation(BaseModel): - """Model representing a document extraction task creation.""" - - extraction_response: ExtractionResponseIXP - action_title: str - action_catalog: str | None = None - action_priority: ActionPriority | None = None - action_folder: str | None = None - storage_bucket_name: str | None = None - storage_bucket_directory_path: str | None = None - - -class WaitDocumentExtractionValidation(BaseModel): - """Model representing a wait document extraction task creation.""" - - extraction_validation: StartExtractionValidationResponse - task_url: str | None = None - - -class WaitIntegrationEvent(BaseModel): - """Model representing a wait on an Integration Services event. - - Used to suspend a job until a remote event (e.g. Slack message, Teams reply) - is delivered by Integration Services. The SDK resolves `connection_name` - (scoped to `connection_folder_path` when provided) to the underlying - connection id and generates a fresh `inbox_id` when the trigger is created; - the rest of the fields describe which remote event to subscribe to via - the Connections service. +__all__ = [ + "CreateBatchTransform", + "CreateDeepRag", + "CreateDeepRagRaw", + "CreateEphemeralIndex", + "CreateEphemeralIndexRaw", + "CreateEscalation", + "CreateTask", + "DocumentExtraction", + "DocumentExtractionValidation", + "InvokeProcess", + "InvokeProcessRaw", + "InvokeSystemAgent", + "WaitBatchTransform", + "WaitDeepRag", + "WaitDeepRagRaw", + "WaitDocumentExtraction", + "WaitDocumentExtractionValidation", + "WaitEphemeralIndex", + "WaitEphemeralIndexRaw", + "WaitEscalation", + "WaitIntegrationEvent", + "WaitJob", + "WaitJobRaw", + "WaitSystemAgent", + "WaitTask", + "WaitUntil", +] + + +def _resolve(name: str) -> Any: + """Resolve a deprecated alias from the canonical module and warn. + + Called only from a module ``__getattr__`` (here or in ``common``), so the + frame three levels up from ``warnings.warn`` is the user's import site. """ + import uipath.platform.resume_triggers.interrupt_models as canonical_models + + value = getattr(canonical_models, name) + warnings.warn( + "Importing interrupt models from uipath.platform.common is deprecated " + "and will be removed in a future release; import from " + "uipath.platform.resume_triggers instead.", + DeprecationWarning, + stacklevel=3, + ) + globals()[name] = value + return value - connector: str - connection_name: str - connection_folder_path: str | None = None - operation: str - object_name: str - filter_expression: str | None = None - parameters: dict[str, str] | None = None - - -class WaitUntil(BaseModel): - """Model representing a wait until an absolute point in time.""" - resume_time: datetime = Field(alias="resumeTime") +def __getattr__(name: str) -> Any: + """Resolve deprecated interrupt model aliases on demand.""" + if name not in __all__: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + return _resolve(name) - model_config = ConfigDict(validate_by_name=True) - @field_validator("resume_time") - @classmethod - def validate_resume_time(cls, value: datetime) -> datetime: - """Validate and normalize resume_time to a UTC instant.""" - if value.tzinfo is None or value.utcoffset() is None: - raise ValueError("resume_time must include timezone information") - return value.astimezone(timezone.utc) +def __dir__() -> list[str]: + """List compatibility exports.""" + return sorted(set(globals()) | set(__all__)) diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py b/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py index b73d810e7..070581aba 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py @@ -16,6 +16,7 @@ from ..common._execution_context import UiPathExecutionContext from ..common._job_context import header_job_key from ..common._models import Endpoint, RequestSpec +from ..common.auth import build_api_config, resolve_config_from_env from ..errors import EnrichedException from .guardrails import BYO_VALIDATOR_TYPE, BuiltInValidatorGuardrail @@ -203,3 +204,11 @@ def evaluate_guardrail( model_data["spanId"] = span_id return GuardrailValidationResult.model_validate(model_data) + + +def default_guardrails_service() -> GuardrailsService: + """Build a guardrails service configured from environment variables.""" + base_url, secret = resolve_config_from_env(None, None) + return GuardrailsService( + build_api_config(base_url, secret), UiPathExecutionContext() + ) diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py index a9eaf5afd..d0cc75f69 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py @@ -7,6 +7,10 @@ from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail +from ..._guardrails_service import ( + GuardrailsService, + default_guardrails_service, +) from .._enums import GuardrailExecutionStage @@ -78,6 +82,8 @@ def get_built_in_guardrail(self, name, description, enabled_for_evals): ) """ + _service: GuardrailsService | None = None + @abstractmethod def get_built_in_guardrail( self, @@ -109,15 +115,13 @@ def run( ) -> GuardrailValidationResult: """Evaluate via the UiPath Guardrails API. - Lazily initialises the ``UiPath`` client on the first call and reuses - it for all subsequent invocations. + Lazily initialises the guardrails service on the first call and + reuses it for all subsequent invocations. """ built_in = self.get_built_in_guardrail(name, description, enabled_for_evals) - if not hasattr(self, "_uipath"): - from uipath.platform import UiPath - - self._uipath: Any = UiPath() - return self._uipath.guardrails.evaluate_guardrail(data, built_in) + if self._service is None: + self._service = default_guardrails_service() + return self._service.evaluate_guardrail(data, built_in) class CustomGuardrailValidator(GuardrailValidatorBase, ABC): diff --git a/packages/uipath-platform/src/uipath/platform/resume_triggers/__init__.py b/packages/uipath-platform/src/uipath/platform/resume_triggers/__init__.py index 47c35f5b7..13ab154ee 100644 --- a/packages/uipath-platform/src/uipath/platform/resume_triggers/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/resume_triggers/__init__.py @@ -6,6 +6,34 @@ UiPathResumeTriggerHandler, UiPathResumeTriggerReader, ) +from .interrupt_models import ( + CreateBatchTransform, + CreateDeepRag, + CreateDeepRagRaw, + CreateEphemeralIndex, + CreateEphemeralIndexRaw, + CreateEscalation, + CreateTask, + DocumentExtraction, + DocumentExtractionValidation, + InvokeProcess, + InvokeProcessRaw, + InvokeSystemAgent, + WaitBatchTransform, + WaitDeepRag, + WaitDeepRagRaw, + WaitDocumentExtraction, + WaitDocumentExtractionValidation, + WaitEphemeralIndex, + WaitEphemeralIndexRaw, + WaitEscalation, + WaitIntegrationEvent, + WaitJob, + WaitJobRaw, + WaitSystemAgent, + WaitTask, + WaitUntil, +) __all__ = [ "UiPathResumeTriggerReader", @@ -14,4 +42,30 @@ "PropertyName", "TriggerMarker", "is_no_content_marker", + "CreateBatchTransform", + "CreateDeepRag", + "CreateDeepRagRaw", + "CreateEphemeralIndex", + "CreateEphemeralIndexRaw", + "CreateEscalation", + "CreateTask", + "DocumentExtraction", + "DocumentExtractionValidation", + "InvokeProcess", + "InvokeProcessRaw", + "InvokeSystemAgent", + "WaitBatchTransform", + "WaitDeepRag", + "WaitDeepRagRaw", + "WaitDocumentExtraction", + "WaitDocumentExtractionValidation", + "WaitEphemeralIndex", + "WaitEphemeralIndexRaw", + "WaitEscalation", + "WaitIntegrationEvent", + "WaitJob", + "WaitJobRaw", + "WaitSystemAgent", + "WaitTask", + "WaitUntil", ] diff --git a/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py b/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py index 784e9f2ae..5dfaff046 100644 --- a/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py +++ b/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py @@ -24,7 +24,25 @@ from uipath.platform.action_center import Task from uipath.platform.action_center.tasks import TaskStatus from uipath.platform.common._config import UiPathConfig -from uipath.platform.common.interrupt_models import ( +from uipath.platform.connections import EventArguments +from uipath.platform.context_grounding import DeepRagStatus, IndexStatus +from uipath.platform.context_grounding.context_grounding_index import ( + ContextGroundingIndex, +) +from uipath.platform.errors import ( + BatchTransformFailedException, + BatchTransformNotCompleteException, + OperationNotCompleteException, +) +from uipath.platform.orchestrator.job import JobState +from uipath.platform.resume_triggers._enums import ( + ExternalTrigger, + ExternalTriggerType, + PropertyName, + TriggerMarker, +) + +from .interrupt_models import ( CreateBatchTransform, CreateDeepRag, CreateDeepRagRaw, @@ -52,23 +70,6 @@ WaitTask, WaitUntil, ) -from uipath.platform.connections import EventArguments -from uipath.platform.context_grounding import DeepRagStatus, IndexStatus -from uipath.platform.context_grounding.context_grounding_index import ( - ContextGroundingIndex, -) -from uipath.platform.errors import ( - BatchTransformFailedException, - BatchTransformNotCompleteException, - OperationNotCompleteException, -) -from uipath.platform.orchestrator.job import JobState -from uipath.platform.resume_triggers._enums import ( - ExternalTrigger, - ExternalTriggerType, - PropertyName, - TriggerMarker, -) def _try_convert_to_json_format(value: str | None) -> Any: diff --git a/packages/uipath-platform/src/uipath/platform/resume_triggers/interrupt_models.py b/packages/uipath-platform/src/uipath/platform/resume_triggers/interrupt_models.py new file mode 100644 index 000000000..ee9104c54 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/resume_triggers/interrupt_models.py @@ -0,0 +1,298 @@ +"""Models for interrupt operations in UiPath platform.""" + +from datetime import datetime, timezone +from typing import Annotated, Any + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +from uipath.platform.context_grounding.context_grounding_index import ( + ContextGroundingIndex, +) + +from ..action_center.tasks import Task, TaskRecipient +from ..attachments import Attachment +from ..context_grounding import ( + BatchTransformCreationResponse, + BatchTransformOutputColumn, + CitationMode, + DeepRagCreationResponse, + EphemeralIndexUsage, +) +from ..documents import ( + ActionPriority, + ExtractionResponseIXP, + FileContent, + StartExtractionResponse, +) +from ..documents.documents import StartExtractionValidationResponse +from ..orchestrator.job import Job + + +class InvokeProcess(BaseModel): + """Model representing a process invocation.""" + + name: str + process_folder_path: str | None = None + process_folder_key: str | None = None + input_arguments: dict[str, Any] | None + attachments: list[Attachment] | None = None + + +class WaitJob(BaseModel): + """Model representing a wait job operation.""" + + job: Job + process_folder_path: str | None = None + process_folder_key: str | None = None + + +class InvokeProcessRaw(InvokeProcess): + """Model representing a raw process invocation (returns job without state validation).""" + + pass + + +class WaitJobRaw(WaitJob): + """Model representing a raw wait job operation (returns job without state validation).""" + + pass + + +class CreateTask(BaseModel): + """Model representing an action creation.""" + + title: str + data: dict[str, Any] | None = None + assignee: str | None = "" + recipient: TaskRecipient | None = None + app_name: str | None = None + app_folder_path: str | None = None + app_folder_key: str | None = None + app_key: str | None = None + priority: str | None = None + labels: list[str] | None = None + is_actionable_message_enabled: bool | None = None + actionable_message_metadata: dict[str, Any] | None = None + source_name: str = "Agent" + + +class CreateEscalation(CreateTask): + """Model representing an escalation creation.""" + + pass + + +class WaitTask(BaseModel): + """Model representing a wait action operation.""" + + action: Task + app_folder_path: str | None = None + app_folder_key: str | None = None + app_name: str | None = None + recipient: TaskRecipient | None = None + + +class WaitEscalation(WaitTask): + """Model representing a wait escalation operation.""" + + pass + + +class CreateDeepRag(BaseModel): + """Model representing a Deep RAG task creation.""" + + name: str + index_name: Annotated[str, Field(max_length=512)] | None = None + index_id: Annotated[str, Field(max_length=512)] | None = None + prompt: Annotated[str, Field(max_length=250000)] + glob_pattern: Annotated[str, Field(max_length=512, default="*")] = "**" + citation_mode: CitationMode = CitationMode.SKIP + index_folder_key: str | None = None + index_folder_path: str | None = None + is_ephemeral_index: bool | None = None + + @model_validator(mode="after") + def validate_ephemeral_index_requires_index_id(self) -> "CreateDeepRag": + """Validate that if it is an ephemeral index that it is using index id.""" + if self.is_ephemeral_index is True and self.index_id is None: + raise ValueError("Index id must be provided for an ephemeral index") + return self + + +class CreateDeepRagRaw(CreateDeepRag): + """Model representing a Deep RAG task creation (returns the deep_rag without status validation).""" + + pass + + +class WaitDeepRag(BaseModel): + """Model representing a wait Deep RAG task.""" + + deep_rag: DeepRagCreationResponse + index_folder_path: str | None = None + index_folder_key: str | None = None + + +class WaitDeepRagRaw(WaitDeepRag): + """Model representing a wait Deep RAG task (returns the deep_rag without status validation).""" + + pass + + +class CreateEphemeralIndex(BaseModel): + """Model representing an Ephemeral Index task creation.""" + + usage: EphemeralIndexUsage + attachments: list[str] + + +class CreateEphemeralIndexRaw(CreateEphemeralIndex): + """Model representing an Ephemeral Index task creation (returns the ephemeral index without status validation).""" + + pass + + +class WaitEphemeralIndex(BaseModel): + """Model representing a wait Ephemeral Index task.""" + + index: ContextGroundingIndex + + +class WaitEphemeralIndexRaw(WaitEphemeralIndex): + """Model representing a wait Ephemeral Index task (returns the ephemeral index without status validation).""" + + pass + + +class CreateBatchTransform(BaseModel): + """Model representing a Batch Transform task creation.""" + + name: str + index_name: str | None = None + index_id: Annotated[str, Field(max_length=512)] | None = None + prompt: Annotated[str, Field(max_length=250000)] + output_columns: list[BatchTransformOutputColumn] + storage_bucket_folder_path_prefix: Annotated[str | None, Field(max_length=512)] = ( + None + ) + enable_web_search_grounding: bool = False + destination_path: str + index_folder_key: str | None = None + index_folder_path: str | None = None + is_ephemeral_index: bool | None = None + + @model_validator(mode="after") + def validate_ephemeral_index_requires_index_id(self) -> "CreateBatchTransform": + """Validate that if it is an ephemeral index that it is using index id.""" + if self.is_ephemeral_index is True and self.index_id is None: + raise ValueError("Index id must be provided for an ephemeral index") + return self + + +class WaitBatchTransform(BaseModel): + """Model representing a wait Batch Transform task.""" + + batch_transform: BatchTransformCreationResponse + index_folder_path: str | None = None + index_folder_key: str | None = None + + +class InvokeSystemAgent(BaseModel): + """Model representing a system agent job invocation.""" + + agent_name: str + entrypoint: str + input_arguments: dict[str, Any] | None = None + folder_path: str | None = None + folder_key: str | None = None + + +class WaitSystemAgent(BaseModel): + """Model representing a wait system agent job invocation.""" + + job_key: str + process_folder_path: str | None = None + process_folder_key: str | None = None + + +class DocumentExtraction(BaseModel): + """Model representing a document extraction task creation.""" + + project_name: str + tag: str + file: FileContent | None = None + file_path: str | None = None + + model_config = ConfigDict( + arbitrary_types_allowed=True, + ) + + @model_validator(mode="after") + def validate_exactly_one_file_source(self) -> "DocumentExtraction": + """Validate that exactly one of file or file_path is provided.""" + if (self.file is None) == (self.file_path is None): + raise ValueError( + "Exactly one of 'file' or 'file_path' must be provided, not both or neither" + ) + return self + + +class WaitDocumentExtraction(BaseModel): + """Model representing a wait document extraction task creation.""" + + extraction: StartExtractionResponse + + +class DocumentExtractionValidation(BaseModel): + """Model representing a document extraction task creation.""" + + extraction_response: ExtractionResponseIXP + action_title: str + action_catalog: str | None = None + action_priority: ActionPriority | None = None + action_folder: str | None = None + storage_bucket_name: str | None = None + storage_bucket_directory_path: str | None = None + + +class WaitDocumentExtractionValidation(BaseModel): + """Model representing a wait document extraction task creation.""" + + extraction_validation: StartExtractionValidationResponse + task_url: str | None = None + + +class WaitIntegrationEvent(BaseModel): + """Model representing a wait on an Integration Services event. + + Used to suspend a job until a remote event (e.g. Slack message, Teams reply) + is delivered by Integration Services. The SDK resolves `connection_name` + (scoped to `connection_folder_path` when provided) to the underlying + connection id and generates a fresh `inbox_id` when the trigger is created; + the rest of the fields describe which remote event to subscribe to via + the Connections service. + """ + + connector: str + connection_name: str + connection_folder_path: str | None = None + operation: str + object_name: str + filter_expression: str | None = None + parameters: dict[str, str] | None = None + + +class WaitUntil(BaseModel): + """Model representing a wait until an absolute point in time.""" + + resume_time: datetime = Field(alias="resumeTime") + + model_config = ConfigDict(validate_by_name=True) + + @field_validator("resume_time") + @classmethod + def validate_resume_time(cls, value: datetime) -> datetime: + """Validate and normalize resume_time to a UTC instant.""" + if value.tzinfo is None or value.utcoffset() is None: + raise ValueError("resume_time must include timezone information") + return value.astimezone(timezone.utc) diff --git a/packages/uipath-platform/tests/common/test_auth.py b/packages/uipath-platform/tests/common/test_auth.py new file mode 100644 index 000000000..8b8fe67a2 --- /dev/null +++ b/packages/uipath-platform/tests/common/test_auth.py @@ -0,0 +1,44 @@ +"""Tests for config helpers in uipath.platform.common.auth.""" + +from unittest.mock import patch + +import pytest +from pydantic import BaseModel, ValidationError + +from uipath.platform.common import UiPathApiConfig +from uipath.platform.common.auth import build_api_config +from uipath.platform.errors import BaseUrlMissingError, SecretMissingError + + +class TestBuildApiConfig: + def test_returns_config_when_both_values_present(self): + config = build_api_config("https://test.uipath.com", "token") + assert isinstance(config, UiPathApiConfig) + assert config.base_url == "https://test.uipath.com" + assert config.secret == "token" + + def test_missing_base_url_raises_typed_error(self): + with pytest.raises(BaseUrlMissingError): + build_api_config(None, "token") + + def test_missing_secret_raises_typed_error(self): + with pytest.raises(SecretMissingError): + build_api_config("https://test.uipath.com", None) + + def test_both_missing_raises_base_url_error_first(self): + with pytest.raises(BaseUrlMissingError): + build_api_config(None, None) + + def test_unmatched_validation_error_is_reraised(self): + class _Other(BaseModel): + other: str + + with pytest.raises(ValidationError) as exc_info: + _Other.model_validate({}) + unmatched = exc_info.value + + with patch( + "uipath.platform.common.auth.UiPathApiConfig", side_effect=unmatched + ): + with pytest.raises(ValidationError): + build_api_config("https://test.uipath.com", "token") diff --git a/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py b/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py new file mode 100644 index 000000000..084bd6702 --- /dev/null +++ b/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py @@ -0,0 +1,101 @@ +import importlib +import subprocess +import sys +import warnings + +# TODO: Remove these tests with the compatibility bridge in the next breaking-change release. +INTERRUPT_MODEL_NAMES = ( + "CreateBatchTransform", + "CreateDeepRag", + "CreateDeepRagRaw", + "CreateEphemeralIndex", + "CreateEphemeralIndexRaw", + "CreateEscalation", + "CreateTask", + "DocumentExtraction", + "DocumentExtractionValidation", + "InvokeProcess", + "InvokeProcessRaw", + "InvokeSystemAgent", + "WaitBatchTransform", + "WaitDeepRag", + "WaitDeepRagRaw", + "WaitDocumentExtraction", + "WaitDocumentExtractionValidation", + "WaitEphemeralIndex", + "WaitEphemeralIndexRaw", + "WaitEscalation", + "WaitIntegrationEvent", + "WaitJob", + "WaitJobRaw", + "WaitSystemAgent", + "WaitTask", + "WaitUntil", +) + + +def _run_python(source: str) -> None: + result = subprocess.run( + [sys.executable, "-c", source], + capture_output=True, + check=False, + text=True, + ) + assert result.returncode == 0, result.stderr + + +def test_compatibility_exports_are_canonical_objects(): + canonical = importlib.import_module( + "uipath.platform.resume_triggers.interrupt_models" + ) + common = importlib.import_module("uipath.platform.common") + legacy = importlib.import_module("uipath.platform.common.interrupt_models") + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + for name in INTERRUPT_MODEL_NAMES: + assert getattr(legacy, name) is getattr(canonical, name) + assert getattr(common, name) is getattr(canonical, name) + + assert set(INTERRUPT_MODEL_NAMES) == set(legacy.__all__) + assert set(INTERRUPT_MODEL_NAMES) <= set(common.__all__) + + +def test_importing_common_does_not_load_resume_triggers_or_warn(): + _run_python( + """ +import sys +import warnings + +with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + import uipath.platform.common + +assert "uipath.platform.resume_triggers" not in sys.modules +assert "uipath.platform.resume_triggers.interrupt_models" not in sys.modules +assert not [item for item in caught if issubclass(item.category, DeprecationWarning)] +""" + ) + + +def test_legacy_first_import_is_cycle_safe_and_warns(): + _run_python( + """ +import warnings + +with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + from uipath.platform.common.interrupt_models import WaitUntil as legacy_direct + from uipath.platform.common import WaitUntil as legacy_root + from uipath.platform.resume_triggers import WaitUntil as canonical + +assert canonical is legacy_root is legacy_direct +compatibility_warnings = [ + item + for item in caught + if issubclass(item.category, DeprecationWarning) + and "uipath.platform.resume_triggers" in str(item.message) +] +assert compatibility_warnings +""" + ) diff --git a/packages/uipath-platform/tests/services/test_guardrails_decorators.py b/packages/uipath-platform/tests/services/test_guardrails_decorators.py index 29674ca09..161d6da5a 100644 --- a/packages/uipath-platform/tests/services/test_guardrails_decorators.py +++ b/packages/uipath-platform/tests/services/test_guardrails_decorators.py @@ -989,8 +989,8 @@ def test_custom_validator_path_delegates_to_run(self): ) assert result == _PASSED - def test_built_in_validator_path_lazy_initializes_uipath(self): - """BuiltInGuardrailValidator.run() lazily creates UiPath() and calls API.""" + def test_built_in_validator_path_lazy_initializes_default_service(self): + """BuiltInGuardrailValidator.run() creates the default service once.""" from uipath.platform.guardrails.decorators.validators import ( BuiltInGuardrailValidator, ) @@ -1005,14 +1005,44 @@ def get_built_in_guardrail(self, name, description, enabled_for_evals): validator = _TestBuiltIn() evaluator = _make_evaluator(validator, "G", None, True) - mock_uipath = MagicMock() - mock_uipath.guardrails.evaluate_guardrail.return_value = _PASSED - with patch("uipath.platform.UiPath", return_value=mock_uipath): + with patch( + "uipath.platform.guardrails.decorators.validators._base.default_guardrails_service" + ) as mock_default: + mock_default.return_value.evaluate_guardrail.return_value = _PASSED evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) - # UiPath() should be created only once despite two calls - assert mock_uipath.guardrails.evaluate_guardrail.call_count == 2 + mock_default.assert_called_once_with() + assert mock_default.return_value.evaluate_guardrail.call_count == 2 + + def test_built_in_validator_without_env_config_raises_missing_errors( + self, monkeypatch + ): + """Missing env credentials surface as typed errors from the decorator path.""" + from uipath.platform.errors import BaseUrlMissingError, SecretMissingError + from uipath.platform.guardrails.decorators.validators import ( + BuiltInGuardrailValidator, + ) + from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail + + class _TestBuiltIn(BuiltInGuardrailValidator): + def get_built_in_guardrail(self, name, description, enabled_for_evals): + return MagicMock(spec=BuiltInValidatorGuardrail) + + for var in ( + "UIPATH_URL", + "UNATTENDED_USER_ACCESS_TOKEN", + "UIPATH_ACCESS_TOKEN", + ): + monkeypatch.delenv(var, raising=False) + + evaluator = _make_evaluator(_TestBuiltIn(), "G", None, True) + with pytest.raises(BaseUrlMissingError): + evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) + + monkeypatch.setenv("UIPATH_URL", "https://test.uipath.com") + with pytest.raises(SecretMissingError): + evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) # --------------------------------------------------------------------------- @@ -1219,8 +1249,8 @@ def test_real_validator_block_end_to_end_mocked_backend(self): """Real LLMAsJudgeValidator.run() path with the UiPath API mocked.""" from uipath.platform.guardrails.decorators import LLMAsJudgeValidator - mock_uipath = MagicMock() - mock_uipath.guardrails.evaluate_guardrail.return_value = _FAILED + mock_service = MagicMock() + mock_service.evaluate_guardrail.return_value = _FAILED @guardrail( validator=LLMAsJudgeValidator( @@ -1232,7 +1262,10 @@ def test_real_validator_block_end_to_end_mocked_backend(self): def joke(topic: str) -> str: return f"joke about {topic}" - with patch("uipath.platform.UiPath", return_value=mock_uipath): + with patch( + "uipath.platform.guardrails.decorators.validators._base.default_guardrails_service", + return_value=mock_service, + ): with pytest.raises(GuardrailBlockException): joke("cats") - mock_uipath.guardrails.evaluate_guardrail.assert_called_once() + mock_service.evaluate_guardrail.assert_called_once() diff --git a/packages/uipath-platform/tests/services/test_guardrails_service.py b/packages/uipath-platform/tests/services/test_guardrails_service.py index 6fd20bbc5..121bbd8ab 100644 --- a/packages/uipath-platform/tests/services/test_guardrails_service.py +++ b/packages/uipath-platform/tests/services/test_guardrails_service.py @@ -17,6 +17,7 @@ GuardrailsService, MapEnumParameterValue, ) +from uipath.platform.guardrails._guardrails_service import default_guardrails_service @pytest.fixture @@ -29,6 +30,18 @@ def service( return GuardrailsService(config=config, execution_context=execution_context) +class TestDefaultGuardrailsService: + def test_builds_guardrails_service_from_env(self, monkeypatch): + """The default factory builds a GuardrailsService from env config.""" + monkeypatch.setenv("UIPATH_URL", "https://test.uipath.com") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "token") + + service = default_guardrails_service() + assert isinstance(service, GuardrailsService) + assert service._config.base_url == "https://test.uipath.com" + assert service._config.secret == "token" + + class TestGuardrailsService: """Test GuardrailsService functionality.""" diff --git a/packages/uipath-platform/tests/services/test_hitl.py b/packages/uipath-platform/tests/services/test_hitl.py index 134d75fc0..86c75d78c 100644 --- a/packages/uipath-platform/tests/services/test_hitl.py +++ b/packages/uipath-platform/tests/services/test_hitl.py @@ -18,31 +18,6 @@ from uipath.platform.action_center import Task from uipath.platform.action_center.tasks import TaskStatus -from uipath.platform.common import ( - CreateBatchTransform, - CreateDeepRag, - CreateDeepRagRaw, - CreateEphemeralIndex, - CreateEphemeralIndexRaw, - CreateTask, - DocumentExtraction, - DocumentExtractionValidation, - InvokeProcess, - InvokeProcessRaw, - InvokeSystemAgent, - WaitBatchTransform, - WaitDeepRag, - WaitDeepRagRaw, - WaitDocumentExtractionValidation, - WaitEphemeralIndex, - WaitEphemeralIndexRaw, - WaitIntegrationEvent, - WaitJob, - WaitJobRaw, - WaitSystemAgent, - WaitTask, - WaitUntil, -) from uipath.platform.connections import Connection from uipath.platform.context_grounding import ( BatchTransformCreationResponse, @@ -70,10 +45,33 @@ from uipath.platform.orchestrator import Job, JobErrorInfo from uipath.platform.orchestrator.job import JobState from uipath.platform.resume_triggers import ( + CreateBatchTransform, + CreateDeepRag, + CreateDeepRagRaw, + CreateEphemeralIndex, + CreateEphemeralIndexRaw, + CreateTask, + DocumentExtraction, + DocumentExtractionValidation, + InvokeProcess, + InvokeProcessRaw, + InvokeSystemAgent, PropertyName, TriggerMarker, UiPathResumeTriggerCreator, UiPathResumeTriggerReader, + WaitBatchTransform, + WaitDeepRag, + WaitDeepRagRaw, + WaitDocumentExtractionValidation, + WaitEphemeralIndex, + WaitEphemeralIndexRaw, + WaitIntegrationEvent, + WaitJob, + WaitJobRaw, + WaitSystemAgent, + WaitTask, + WaitUntil, ) diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 802e19ac8..1d4b41b14 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -99,6 +99,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -230,6 +242,99 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" }, ] +[[package]] +name = "grimp" +version = "3.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/46/79764cfb61a3ac80dadae5d94fb10acdb7800e31fecf4113cf3d345e4952/grimp-3.14.tar.gz", hash = "sha256:645fbd835983901042dae4e1b24fde3a89bf7ac152f9272dd17a97e55cb4f871", size = 830882, upload-time = "2025-12-10T17:55:01.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/31/d4a86207c38954b6c3d859a1fc740a80b04bbe6e3b8a39f4e66f9633dfa4/grimp-3.14-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f1c91e3fa48c2196bf62e3c71492140d227b2bfcd6d15e735cbc0b3e2d5308e0", size = 2185572, upload-time = "2025-12-10T17:53:41.287Z" }, + { url = "https://files.pythonhosted.org/packages/f5/61/ed4cba5bd75d37fe46e17a602f616619a9e4f74ad8adfcf560ce4b2a1697/grimp-3.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6291c8f1690a9fe21b70923c60b075f4a89676541999e3d33084cbc69ac06a1", size = 2118002, upload-time = "2025-12-10T17:53:18.546Z" }, + { url = "https://files.pythonhosted.org/packages/77/6a/688f6144d0b207d7845bd8ab403820a83630ce3c9420cbbc7c9e9282f9c0/grimp-3.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ec312383935c2d09e4085c8435780ada2e13ebef14e105609c2988a02a5b2ce", size = 2283939, upload-time = "2025-12-10T17:52:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/a5/98/4c540de151bf3fd58d6d7b3fe2269b6a6af6c61c915de1bc991802bfaff8/grimp-3.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f43cbf640e73ee703ad91639591046828d20103a1c363a02516e77a66a4ac07", size = 2233693, upload-time = "2025-12-10T17:52:18.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/7b/84b4b52b6c6dd5bf083cb1a72945748f56ea2e61768bbebf87e8d9d0ef75/grimp-3.14-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a93c9fddccb9ff16f5c6b5fca44227f5f86cba7cffc145d2176119603d2d7c7", size = 2389745, upload-time = "2025-12-10T17:53:00.659Z" }, + { url = "https://files.pythonhosted.org/packages/a7/33/31b96907c7dd78953df5e1ce67c558bd6057220fa1203d28d52566315a2e/grimp-3.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5653a2769fdc062cb7598d12200352069c9c6559b6643af6ada3639edb98fcc3", size = 2569055, upload-time = "2025-12-10T17:52:33.556Z" }, + { url = "https://files.pythonhosted.org/packages/b2/24/ce1a8110f3d5b178153b903aafe54b6a9216588b5bff3656e30af43e9c29/grimp-3.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:071c7ddf5e5bb7b2fdf79aefdf6e1c237cd81c095d6d0a19620e777e85bf103c", size = 2358044, upload-time = "2025-12-10T17:52:47.545Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/16d98c02287bc99884843478b9a68b04a2ef13b5cb8b9f36a9ca7daea75b/grimp-3.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e01b7a4419f535b667dfdcb556d3815b52981474f791fb40d72607228389a31", size = 2310304, upload-time = "2025-12-10T17:53:09.679Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/0fde9781b0f6b4f9227d485685f48f6bcc70b95af22e2f85ff7f416cbfc1/grimp-3.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c29682f336151d1d018d0c3aa9eeaa35734b970e4593fa396b901edca7ef5c79", size = 2463682, upload-time = "2025-12-10T17:53:49.185Z" }, + { url = "https://files.pythonhosted.org/packages/51/cb/2baff301c2c2cc2792b6e225ea0784793ca587c81b97572be0bad122cfc8/grimp-3.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a5c4fd71f363ea39e8aab0630010ced77a8de9789f27c0acdd0d7e6269d4a8ef", size = 2500573, upload-time = "2025-12-10T17:54:03.899Z" }, + { url = "https://files.pythonhosted.org/packages/96/69/797e4242f42d6665da5fe22cb250cae3f14ece4cb22ad153e9cd97158179/grimp-3.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766911e3ba0b13d833fdd03ad1f217523a8a2b2527b5507335f71dca1153183d", size = 2503005, upload-time = "2025-12-10T17:54:32.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/45/da1a27a6377807ca427cd56534231f0920e1895e16630204f382a0df14c5/grimp-3.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:154e84a2053e9f858ae48743de23a5ad4eb994007518c29371276f59b8419036", size = 2515776, upload-time = "2025-12-10T17:54:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/b918a29ce98029cd7a9e33a584be43a93288d5283fb7ccef5b6b2ba39ede/grimp-3.14-cp311-cp311-win32.whl", hash = "sha256:3189c86c3e73016a1907ee3ba9f7a6ca037e3601ad09e60ce9bf12b88877f812", size = 1873189, upload-time = "2025-12-10T17:55:11.872Z" }, + { url = "https://files.pythonhosted.org/packages/90/d7/2327c203f83a25766fbd62b0df3b24230d422b6e53518ff4d1c5e69793f1/grimp-3.14-cp311-cp311-win_amd64.whl", hash = "sha256:201f46a6a4e5ee9dfba4a2f7d043f7deab080d1d84233f4a1aee812678c25307", size = 2014277, upload-time = "2025-12-10T17:55:04.144Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/a35ff62f35aa5fd148053506eddd7a8f2f6afaed31870dc608dd0eb38e4f/grimp-3.14-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ffabc6940301214753bad89ec0bfe275892fa1f64b999e9a101f6cebfc777133", size = 2178573, upload-time = "2025-12-10T17:53:42.836Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/bd2e80273da4d46110969fc62252e5372e0249feb872bc7fe76fdc7f1818/grimp-3.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:075d9a1c78d607792d0ed8d4d3d7754a621ef04c8a95eaebf634930dc9232bb2", size = 2110452, upload-time = "2025-12-10T17:53:19.831Z" }, + { url = "https://files.pythonhosted.org/packages/44/c3/7307249c657d34dca9d250d73ba027d6cfe15a98fb3119b6e5210bc388b7/grimp-3.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06ff52addeb20955a4d6aa097bee910573ffc9ef0d3c8a860844f267ad958156", size = 2283064, upload-time = "2025-12-10T17:52:07.673Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d2/cae4cf32dc8d4188837cc4ab183300d655f898969b0f169e240f3b7c25be/grimp-3.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d10e0663e961fcbe8d0f54608854af31f911f164c96a44112d5173050132701f", size = 2235893, upload-time = "2025-12-10T17:52:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/3f58bc3064fc305dac107d08003ba65713a5bc89a6d327f1c06b30cce752/grimp-3.14-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ab874d7ddddc7a1291259cf7c31a4e7b5c612e9da2e24c67c0eb1a44a624e67", size = 2393376, upload-time = "2025-12-10T17:53:02.397Z" }, + { url = "https://files.pythonhosted.org/packages/06/b8/f476f30edf114f04cb58e8ae162cb4daf52bda0ab01919f3b5b7edb98430/grimp-3.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54fec672ec83355636a852177f5a470c964bede0f6730f9ba3c7b5c8419c9eab", size = 2571342, upload-time = "2025-12-10T17:52:35.214Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ae/2e44d3c4f591f95f86322a8f4dbb5aac17001d49e079f3a80e07e7caaf09/grimp-3.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9e221b5e8070a916c780e88c877fee2a61c95a76a76a2a076396e459511b0bb", size = 2359022, upload-time = "2025-12-10T17:52:49.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/42b4d6bc0ea119ce2e91e1788feabf32c5433e9617dbb495c2a3d0dc7f12/grimp-3.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eea6b495f9b4a8d82f5ce544921e76d0d12017f5d1ac3a3bd2f5ac88ab055b1c", size = 2309424, upload-time = "2025-12-10T17:53:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c7/6a731989625c1790f4da7602dcbf9d6525512264e853cda77b3b3602d5e0/grimp-3.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:655e8d3f79cd99bb859e09c9dd633515150e9d850879ca71417d5ac31809b745", size = 2462754, upload-time = "2025-12-10T17:53:50.886Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/3d1571c0a39a59dd68be4835f766da64fe64cbab0d69426210b716a8bdf0/grimp-3.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a14f10b1b71c6c37647a76e6a49c226509648107abc0f48c1e3ecd158ba05531", size = 2501356, upload-time = "2025-12-10T17:54:06.014Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d1/8950b8229095ebda5c54c8784e4d1f0a6e19423f2847289ef9751f878798/grimp-3.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:81685111ee24d3e25f8ed9e77ed00b92b58b2414e1a1c2937236026900972744", size = 2504631, upload-time = "2025-12-10T17:54:34.441Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/23bed3da9206138d36d01890b656c7fb7adfb3a37daac8842d84d8777ade/grimp-3.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce8352a8ea0e27b143136ea086582fc6653419aa8a7c15e28ed08c898c42b185", size = 2514751, upload-time = "2025-12-10T17:54:49.384Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/6f1f55c97ee982f133ec5ccb22fc99bf5335aee70c208f4fb86cd833b8d5/grimp-3.14-cp312-cp312-win32.whl", hash = "sha256:3fc0f98b3c60d88e9ffa08faff3200f36604930972f8b29155f323b76ea25a06", size = 1875041, upload-time = "2025-12-10T17:55:13.326Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/03ba01288e2a41a948bc8526f32c2eeaddd683ed34be1b895e31658d5a4c/grimp-3.14-cp312-cp312-win_amd64.whl", hash = "sha256:6bca77d1d50c8dc402c96af21f4e28e2f1e9938eeabd7417592a22bd83cde3c3", size = 2013868, upload-time = "2025-12-10T17:55:05.907Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bd/d12a9c821b79ba31fc52243e564712b64140fc6d011c2bdbb483d9092a12/grimp-3.14-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af8a625554beea84530b98cc471902155b5fc042b42dc47ec846fa3e32b0c615", size = 2178632, upload-time = "2025-12-10T17:53:44.55Z" }, + { url = "https://files.pythonhosted.org/packages/96/8c/d6620dbc245149d5a5a7a9342733556ba91a672f358259c0ab31d889b56b/grimp-3.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0dd1942ffb419ad342f76b0c3d3d2d7f312b264ddc578179d13ce8d5acec1167", size = 2110288, upload-time = "2025-12-10T17:53:21.662Z" }, + { url = "https://files.pythonhosted.org/packages/60/9d/ea51edc4eb295c99786040051c66466bfa235fd1def9f592057b36e03d0f/grimp-3.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:537f784ce9b4acf8657f0b9714ab69a6c72ffa752eccc38a5a85506103b1a194", size = 2282197, upload-time = "2025-12-10T17:52:09.304Z" }, + { url = "https://files.pythonhosted.org/packages/28/6e/7db27818ced6a797f976ca55d981a3af5c12aec6aeda12d63965847cd028/grimp-3.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:78ab18c08770aa005bef67b873bc3946d33f65727e9f3e508155093db5fa57d6", size = 2235720, upload-time = "2025-12-10T17:52:21.806Z" }, + { url = "https://files.pythonhosted.org/packages/37/26/0e3bbae4826bd6eaabf404738400414071e73ddb1e65bf487dcce17858c4/grimp-3.14-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28ca58728c27e7292c99f964e6ece9295c2f9cfdefc37c18dea0679c783ffb6f", size = 2393023, upload-time = "2025-12-10T17:53:04.149Z" }, + { url = "https://files.pythonhosted.org/packages/49/f2/7da91db5703da34c7ef4c7cddcbb1a8fc30cd85fe54756eba942c6fb27d8/grimp-3.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b5577de29c6c5ae6e08d4ca0ac361b45dba323aa145796e6b320a6ea35414b7", size = 2571108, upload-time = "2025-12-10T17:52:36.523Z" }, + { url = "https://files.pythonhosted.org/packages/25/5e/4d6278f18032c7208696edf8be24a4b5f7fad80acc20ffca737344bcecb5/grimp-3.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d7d1f9f42306f455abcec34db877e4887ff15f2777a43491f7ccbd6936c449b", size = 2358531, upload-time = "2025-12-10T17:52:50.521Z" }, + { url = "https://files.pythonhosted.org/packages/24/fb/231c32493161ac82f27af6a56965daefa0ec6030fdaf5b948ddd5d68d000/grimp-3.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39bd5c9b7cef59ee30a05535e9cb4cbf45a3c503f22edce34d0aa79362a311a9", size = 2308831, upload-time = "2025-12-10T17:53:12.587Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/f6db325bf5efbbebc9c85cad0af865e821a12a0ba58ee309e938cbd5fedf/grimp-3.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7fec3116b4f780a1bc54176b19e6b9f2e36e2ef3164b8fc840660566af35df88", size = 2462138, upload-time = "2025-12-10T17:53:52.403Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/cc3fe29cf07f70364018086840c228a190539ab8105147e34588db590792/grimp-3.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0233a35a5bbb23688d63e1736b54415fa9994ace8dfeb7de8514ed9dee212968", size = 2501393, upload-time = "2025-12-10T17:54:22.486Z" }, + { url = "https://files.pythonhosted.org/packages/e5/eb/54cada9a726455148da23f64577b5cd164164d23a6449e3fa14551157356/grimp-3.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e46b2fef0f1da7e7e2f8129eb93c7e79db716ff7810140a22ce5504e10ed86df", size = 2504514, upload-time = "2025-12-10T17:54:36.34Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c7/e6afe4f0652df07e8762f61899d1202b73c22c559c804d0a09e5aab2ff17/grimp-3.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e6d9b50623ee1c3d2a1927ec3f5d408995ea1f92f3e91ed996c908bb40e856f", size = 2514018, upload-time = "2025-12-10T17:54:50.76Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/2b8550acc1f010301f02c4fe9664810929fd9277cd032ab608b8534a96fb/grimp-3.14-cp313-cp313-win32.whl", hash = "sha256:fd57c56f5833c99320ec77e8ba5508d56f6fb48ec8032a942f7931cc6ebb80ce", size = 1874922, upload-time = "2025-12-10T17:55:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/46/c7/bc9db5a54ef22972cd17d15ad80a8fee274a471bd3f02300405702d29ea5/grimp-3.14-cp313-cp313-win_amd64.whl", hash = "sha256:173307cf881a126fe5120b7bbec7d54384002e3c83dcd8c4df6ce7f0fee07c53", size = 2013705, upload-time = "2025-12-10T17:55:07.488Z" }, + { url = "https://files.pythonhosted.org/packages/80/7e/02710bf5e50997168c84ac622b10dd41d35515efd0c67549945ad20996a0/grimp-3.14-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebe29f8f13fbd7c314908ed535183a36e6db71839355b04869b27f23c58fa082", size = 2281868, upload-time = "2025-12-10T17:52:10.589Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/2e440c6762cc78bd50582e1b092357d2255f0852ccc6218d8db25170ab31/grimp-3.14-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073d285b00100153fd86064c7726bb1b6d610df1356d33bb42d3fd8809cb6e72", size = 2230917, upload-time = "2025-12-10T17:52:23.212Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bb/2e7dce129b88f07fc525fe5c97f28cfb7ed7b62c59386d39226b4d08969c/grimp-3.14-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6d6efc37e1728bbfcd881b89467be5f7b046292597b3ebe5f8e44e89ea8b6cb", size = 2571371, upload-time = "2025-12-10T17:52:37.84Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2b/8f1be8294af60c953687db7dec25525d87ed9c2aa26b66dcbe5244abaca2/grimp-3.14-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5337d65d81960b712574c41e85b480d4480bbb5c6f547c94e634f6c60d730889", size = 2356980, upload-time = "2025-12-10T17:52:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/35/ca/ead91e04b3ddd4774ae74601860ea0f0f21bcf6b970b6769ba9571eb2904/grimp-3.14-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:84a7fea63e352b325daa89b0b7297db411b7f0036f8d710c32f8e5090e1fc3ca", size = 2461540, upload-time = "2025-12-10T17:53:53.749Z" }, + { url = "https://files.pythonhosted.org/packages/94/aa/f8a085ff73c37d6e6a37de9f58799a3fea9e16badf267aaef6f11c9a53a3/grimp-3.14-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d0b19a3726377165fe1f7184a8af317734d80d32b371b6c5578747867ab53c0b", size = 2497925, upload-time = "2025-12-10T17:54:23.842Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a3/db3c2d6df07fe74faf5a28fcf3b44fad2831d323ba4a3c2ff66b77a6520c/grimp-3.14-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9caa4991f530750f88474a3f5ecf6ef9f0d064034889d92db00cfb4ecb78aa24", size = 2501794, upload-time = "2025-12-10T17:54:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/095f4e3765e7b60425a41e9fbd2b167f8b0acb957cc88c387f631778a09d/grimp-3.14-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1876efc119b99332a5cc2b08a6bdaada2f0ad94b596f0372a497e2aa8bda4d94", size = 2515203, upload-time = "2025-12-10T17:54:52.555Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5f/ee02a3a1237282d324f596a50923bf9d2cb1b1230ef2fef49fb4d3563c2c/grimp-3.14-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3ccf03e65864d6bc7bf1c003c319f5330a7627b3677f31143f11691a088464c2", size = 2177150, upload-time = "2025-12-10T17:53:46.145Z" }, + { url = "https://files.pythonhosted.org/packages/f2/64/2a92889e5fc78e8ef5c548e6a5c6fed78b817eeb0253aca586c28108393a/grimp-3.14-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9ecd58fa58a270e7523f8bec9e6452f4fdb9c21e4cd370640829f1e43fa87a69", size = 2109280, upload-time = "2025-12-10T17:53:23.345Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/5d0b9ab54821e7fbdeb02f3919fa2cb8b9f0c3869fa6e4b969a5766f0ffa/grimp-3.14-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d75d1f8f7944978b39b08d870315174f1ffcd5123be6ccff8ce90467ace648a", size = 2283367, upload-time = "2025-12-10T17:52:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/c2/96/a77c40c92faf7500f42ac019ab8de108b04ffe3db8ec8d6f90416d2322ce/grimp-3.14-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f70bbb1dd6055d08d29e39a78a11c4118c1778b39d17cd8271e18e213524ca7", size = 2237125, upload-time = "2025-12-10T17:52:24.606Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5e/3e1483721c83057bff921cf454dd5ff3e661ae1d2e63150a380382d116c2/grimp-3.14-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f21b7c003626c902669dc26ede83a91220cf0a81b51b27128370998c2f247b4", size = 2391735, upload-time = "2025-12-10T17:53:05.619Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cb/25fad4a174fe672d42f3e5616761a8120a3b03c8e9e2ae3f31159561968a/grimp-3.14-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80d9f056415c936b45561310296374c4319b5df0003da802c84d2830a103792a", size = 2571388, upload-time = "2025-12-10T17:52:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/7e/456df7f6a765ce3f160eb32a0f64ed0c1c3cd39b518555dde02087f9b6e4/grimp-3.14-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0332963cd63a45863775d4237e59dedf95455e0a1ea50c356be23100c5fc1d7c", size = 2359637, upload-time = "2025-12-10T17:52:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/7c/98/3e5005ef21a4e2243f0da489aba86aaaff0bc11d5240d67113482cba88e0/grimp-3.14-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4144350d074f2058fe7c89230a26b34296b161f085b0471a692cb2fe27036f", size = 2308335, upload-time = "2025-12-10T17:53:13.893Z" }, + { url = "https://files.pythonhosted.org/packages/8a/03/4e055f756946d6f71ab7e9d1f8536a9e476777093dd7a050f40412d1a2b1/grimp-3.14-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e148e67975e92f90a8435b1b4c02180b9a3f3d725b7a188ba63793f1b1e445a0", size = 2463680, upload-time = "2025-12-10T17:53:55.507Z" }, + { url = "https://files.pythonhosted.org/packages/26/b9/3c76b7c2e1587e4303a6eff6587c2117c3a7efe1b100cd13d8a4a5613572/grimp-3.14-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1093f7770cb5f3ca6f99fb152f9c949381cc0b078dfdfe598c8ab99abaccda3b", size = 2502808, upload-time = "2025-12-10T17:54:25.383Z" }, + { url = "https://files.pythonhosted.org/packages/20/80/ada10b85ad3125ebedea10256d9c568b6bf28339d2f79d2d196a7b94f633/grimp-3.14-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a213f45ec69e9c2b28ffd3ba5ab12cc9859da17083ba4dc39317f2083b618111", size = 2504013, upload-time = "2025-12-10T17:54:39.762Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/7c369f749d50b0ceac23cd6874ca4695cc1359a96091c7010301e5c8b619/grimp-3.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f003ac3f226d2437a49af0b6036f26edba57f8a32d329275dbde1b2b2a00a56", size = 2515043, upload-time = "2025-12-10T17:54:54.437Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/85135fe83826ce11ae56a340d32a1391b91eed94d25ce7bc318019f735de/grimp-3.14-cp314-cp314-win32.whl", hash = "sha256:eec81be65a18f4b2af014b1e97296cc9ee20d1115529bf70dd7e06f457eac30b", size = 1877509, upload-time = "2025-12-10T17:55:17.062Z" }, + { url = "https://files.pythonhosted.org/packages/db/61/e4a2234edecb3bb3cff8963bc4ec5cc482a9e3c54f8df0946d7d90003830/grimp-3.14-cp314-cp314-win_amd64.whl", hash = "sha256:cd3bab6164f1d5e313678f0ab4bf45955afe7f5bdb0f2f481014aa9cca7e81ba", size = 2014364, upload-time = "2025-12-10T17:55:08.896Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/3d304443fbf1df4d60c09668846d0c8a605c6c95646226e41d8f5c3254da/grimp-3.14-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1df33de479be4d620f69633d1876858a8e64a79c07907d47cf3aaf896af057", size = 2281385, upload-time = "2025-12-10T17:52:13.668Z" }, + { url = "https://files.pythonhosted.org/packages/fe/13/493e2648dbb83b3fc517ee675e464beb0154551d726053c7982a3138c6a8/grimp-3.14-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07096d4402e9d5a2c59c402ea3d601f4b7f99025f5e32f077468846fc8d3821b", size = 2231470, upload-time = "2025-12-10T17:52:26.104Z" }, + { url = "https://files.pythonhosted.org/packages/80/84/e772b302385a6b7ec752c88f84ffe35c33d14076245ae27a635aed9c63a2/grimp-3.14-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:712bc28f46b354316af50c469c77953ba3d6cb4166a62b8fb086436a8b05d301", size = 2571579, upload-time = "2025-12-10T17:52:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/69/92/5b23aa7b89c5f4f2cfa636cbeaf33e784378a6b0a823d77a3448670dfacc/grimp-3.14-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abe2bbef1cf8e27df636c02f60184319f138dee4f3a949405c21a4b491980397", size = 2356545, upload-time = "2025-12-10T17:52:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/bcf2116f4b1c3939ab35f9cdddd9ca59e953e57e9a0ac0c143deaf9f29cc/grimp-3.14-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2f9ae3fabb7a7a8468ddc96acc84ecabd84f168e7ca508ee94d8f32ea9bd5de2", size = 2461022, upload-time = "2025-12-10T17:53:56.923Z" }, + { url = "https://files.pythonhosted.org/packages/81/ce/1a076dce6bc22bca4b9ad5d1bbcd7e1023dcf7bf20ea9404c6462d78f049/grimp-3.14-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:efaf11ea73f7f12d847c54a5d6edcbe919e0369dce2d1aabae6c50792e16f816", size = 2498256, upload-time = "2025-12-10T17:54:27.214Z" }, + { url = "https://files.pythonhosted.org/packages/45/ea/ac735bed202c1c5c019e611b92d3861779e0cfbe2d20fdb0dec94266d248/grimp-3.14-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e089c9ab8aa755ff5af88c55891727783b4eb6b228e7bdf278e17209d954aa1e", size = 2502056, upload-time = "2025-12-10T17:54:41.537Z" }, + { url = "https://files.pythonhosted.org/packages/80/8f/774ce522de6a7e70fbeceeaeb6fbe502f5dfb8365728fb3bb4cb23463da8/grimp-3.14-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a424ad14d5deb56721ac24ab939747f72ab3d378d42e7d1f038317d33b052b77", size = 2515157, upload-time = "2025-12-10T17:54:55.874Z" }, + { url = "https://files.pythonhosted.org/packages/65/cc/dbc00210d0324b8fc1242d8e857757c7e0b62ff0fc0c1bc8dcc42342da85/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c8a8aab9b4310a7e69d7d845cac21cf14563aa0520ea322b948eadeae56d303", size = 2284804, upload-time = "2025-12-10T17:52:16.379Z" }, + { url = "https://files.pythonhosted.org/packages/80/89/851d3d345342e9bcec3fe85d3997db29501fa59f958c1566bf3e24d9d7d9/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d781943b27e5875a41c8f9cfc80f8f0a349f864379192b8c3faa0e6a22593313", size = 2235176, upload-time = "2025-12-10T17:52:30.795Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/5f94702a8d5c121cafcdc9664de34c34f19d0d91a1127bf3946a2631f7a3/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9630d4633607aff94d0ac84b9c64fef1382cdb05b00d9acbde47f8745e264871", size = 2391258, upload-time = "2025-12-10T17:53:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a2/df8c79de5c9e227856d048cc1551c4742a5f97660c40304ac278bd48607f/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cb00e1bcca583668554a8e9e1e4229a1d11b0620969310aae40148829ff6a32", size = 2571443, upload-time = "2025-12-10T17:52:43.853Z" }, + { url = "https://files.pythonhosted.org/packages/f0/21/747b7ed9572bbdc34a76dfec12ce510e80164b1aa06d3b21b34994e5f567/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3389da4ceaaa7f7de24a668c0afc307a9f95997bd90f81ec359a828a9bd1d270", size = 2357767, upload-time = "2025-12-10T17:52:57.84Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e6/485c5e3b64933e71f72f0cc45b0d7130418a6a5a13cedc2e8411bd76f290/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd7a32970ef97e42d4e7369397c7795287d84a736d788ccb90b6c14f0561d975", size = 2309069, upload-time = "2025-12-10T17:53:15.203Z" }, + { url = "https://files.pythonhosted.org/packages/31/bd/12024a8cba1c77facc1422a7b48cd0d04c252fc9178fd6f99dc05a8af57b/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:fd1278623fa09f62abc0fd8a6500f31b421a1fd479980f44c2926020a0becf02", size = 2466429, upload-time = "2025-12-10T17:54:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7f/0e5977887e1c8f00f84bb4125217534806ffdcef9cf52f3580aa3b151f4b/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:9cfa52c89333d3d8fe9dc782529e888270d060231c3783e036d424044671dde0", size = 2501190, upload-time = "2025-12-10T17:54:30.107Z" }, + { url = "https://files.pythonhosted.org/packages/42/6b/06acb94b6d0d8c7277bb3e33f93224aa3be5b04643f853479d3bf7b23ace/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:48a5be4a12fca6587e6885b4fc13b9e242ab8bf874519292f0f13814aecf52cc", size = 2503440, upload-time = "2025-12-10T17:54:44.444Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4d/2e531370d12e7a564f67f680234710bbc08554238a54991cd244feb61fb6/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3fcc332466783a12a42cd317fd344c30fe734ba4fa2362efff132dc3f8d36da7", size = 2516525, upload-time = "2025-12-10T17:54:58.987Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -285,6 +390,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "import-linter" +version = "2.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "grimp" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/a7/5ae6dc1caf8229f9bcd05b0dd71aba6d5b699b81e5d599fd05bb36046e2a/import_linter-2.12.tar.gz", hash = "sha256:217f0f0f0890b8625389765039d52c215f4c6d4a047f42ea78b6d00ceba1900e", size = 1173831, upload-time = "2026-06-23T08:58:35.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2b/3d9ff17662d01aa83fad076f4881d95136c72781b6b7ef8bf0464a5037a7/import_linter-2.12-py3-none-any.whl", hash = "sha256:b346eeb30cabd5b0630773d42cca4eac8dd459ac5d507ac2ecadc3f99462f231", size = 637947, upload-time = "2026-06-23T08:58:33.969Z" }, +] + [[package]] name = "importlib-metadata" version = "8.7.1" @@ -1095,7 +1215,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.9" +version = "0.2.10" source = { editable = "." } dependencies = [ { name = "httpx" }, @@ -1109,6 +1229,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "bandit" }, + { name = "import-linter" }, { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, @@ -1134,6 +1255,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "bandit", specifier = ">=1.8.2" }, + { name = "import-linter", specifier = ">=2.0" }, { name = "mypy", specifier = ">=1.19.0" }, { name = "pre-commit", specifier = ">=4.1.0" }, { name = "pytest", specifier = ">=7.4.0" }, diff --git a/packages/uipath/tests/cli/unit/test_runtime_protocol_compat.py b/packages/uipath/tests/cli/unit/test_runtime_protocol_compat.py index 812ddaa11..efb0aafd0 100644 --- a/packages/uipath/tests/cli/unit/test_runtime_protocol_compat.py +++ b/packages/uipath/tests/cli/unit/test_runtime_protocol_compat.py @@ -9,8 +9,7 @@ UiPathResumeTriggerName, UiPathResumeTriggerType, ) -from uipath.platform.common import WaitUntil -from uipath.platform.resume_triggers import UiPathResumeTriggerHandler +from uipath.platform.resume_triggers import UiPathResumeTriggerHandler, WaitUntil from uipath.runtime import ( UiPathExecuteOptions, UiPathResumableRuntime, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index da12487fe..657d0bd59 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2741,7 +2741,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.9" +version = "0.2.10" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, @@ -2765,6 +2765,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "bandit", specifier = ">=1.8.2" }, + { name = "import-linter", specifier = ">=2.0" }, { name = "mypy", specifier = ">=1.19.0" }, { name = "pre-commit", specifier = ">=4.1.0" }, { name = "pytest", specifier = ">=7.4.0" },