diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index eab210d34..b12e287e3 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.17" +version = "0.2.18" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/entities/__init__.py b/packages/uipath-platform/src/uipath/platform/entities/__init__.py index 8c323eca3..4cfcf0b5f 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/entities/__init__.py @@ -8,11 +8,19 @@ from .entities import ( AggregateRow, ChoiceSetValue, + DataDirectionType, DataFabricEntityItem, Entity, EntityAggregate, EntityAggregateFunction, EntityBinning, + EntityClass, + EntityClassId, + EntityCreateExternalConnection, + EntityCreateExternalField, + EntityCreateExternalFieldMapping, + EntityCreateExternalObject, + EntityCreateExternalSource, EntityCreateFieldOptions, EntityCreateOptions, EntityField, @@ -35,17 +43,24 @@ FailureRecord, FieldDataType, FieldMetadata, + JoinType, LogicalOperator, + NativeConnectionDetail, QueryFilterOperator, QueryRoutingOverrideContext, ReferenceType, RetrieveEntityRecordsResponse, + Searchability, + SearchabilityNamedSearch, + SearchabilityOperator, + SourceJoinConditionDetail, SourceJoinCriteria, ) __all__ = [ "AggregateRow", "ChoiceSetValue", + "DataDirectionType", "DataFabricEntityItem", "DataFabricOntologyItem", "EntitiesService", @@ -53,6 +68,13 @@ "EntityAggregate", "EntityAggregateFunction", "EntityBinning", + "EntityClass", + "EntityClassId", + "EntityCreateExternalConnection", + "EntityCreateExternalField", + "EntityCreateExternalFieldMapping", + "EntityCreateExternalObject", + "EntityCreateExternalSource", "EntityCreateFieldOptions", "EntityCreateOptions", "EntityField", @@ -75,10 +97,16 @@ "FailureRecord", "FieldDataType", "FieldMetadata", + "JoinType", "LogicalOperator", + "NativeConnectionDetail", "QueryFilterOperator", "QueryRoutingOverrideContext", "ReferenceType", "RetrieveEntityRecordsResponse", + "Searchability", + "SearchabilityNamedSearch", + "SearchabilityOperator", + "SourceJoinConditionDetail", "SourceJoinCriteria", ] diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entity_data_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entity_data_service.py index 4dc09855d..7539d4973 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/_entity_data_service.py +++ b/packages/uipath-platform/src/uipath/platform/entities/_entity_data_service.py @@ -737,9 +737,7 @@ def _list_records_spec( params["$expand"] = ",".join(expand) return RequestSpec( method="GET", - endpoint=Endpoint( - f"datafabric_/api/EntityService/entity/{entity_key}/read" - ), + endpoint=Endpoint(f"datafabric_/api/v3/entities/entity/{entity_key}/read"), params=params, ) @@ -756,7 +754,7 @@ def _insert_record_spec( return RequestSpec( method="POST", endpoint=Endpoint( - f"datafabric_/api/EntityService/entity/{entity_key}/insert" + f"datafabric_/api/v3/entities/entity/{entity_key}/insert" ), params=params, json=EntityDataService._record_to_dict(data), @@ -775,7 +773,7 @@ def _get_record_spec( return RequestSpec( method="GET", endpoint=Endpoint( - f"datafabric_/api/EntityService/entity/{entity_key}/read/{record_id}" + f"datafabric_/api/v3/entities/entity/{entity_key}/read/{record_id}" ), params=params, ) @@ -794,7 +792,7 @@ def _update_record_spec( return RequestSpec( method="POST", endpoint=Endpoint( - f"datafabric_/api/EntityService/entity/{entity_key}/update/{record_id}" + f"datafabric_/api/v3/entities/entity/{entity_key}/update/{record_id}" ), params=params, json=EntityDataService._record_to_dict(data), @@ -806,7 +804,7 @@ def _delete_record_spec(entity_key: str, record_id: str) -> RequestSpec: return RequestSpec( method="DELETE", endpoint=Endpoint( - f"datafabric_/api/EntityService/entity/{entity_key}/delete/{record_id}" + f"datafabric_/api/v3/entities/entity/{entity_key}/delete/{record_id}" ), ) @@ -824,7 +822,7 @@ def _insert_batch_spec( return RequestSpec( method="POST", endpoint=Endpoint( - f"datafabric_/api/EntityService/entity/{entity_key}/insert-batch" + f"datafabric_/api/v3/entities/entity/{entity_key}/insert-batch" ), params=params, json=[EntityDataService._record_to_dict(record) for record in records], @@ -844,7 +842,7 @@ def _update_batch_spec( return RequestSpec( method="POST", endpoint=Endpoint( - f"datafabric_/api/EntityService/entity/{entity_key}/update-batch" + f"datafabric_/api/v3/entities/entity/{entity_key}/update-batch" ), params=params, json=records, @@ -861,7 +859,7 @@ def _delete_batch_spec( return RequestSpec( method="POST", endpoint=Endpoint( - f"datafabric_/api/EntityService/entity/{entity_key}/delete-batch" + f"datafabric_/api/v3/entities/entity/{entity_key}/delete-batch" ), params=params, json=record_ids, @@ -946,13 +944,19 @@ def _retrieve_records_spec( if expansion_level is not None: params["expansionLevel"] = expansion_level - if binnings: + # Route the query. Multi-entity joins stay on the v1 by-key endpoint: the + # v3 by-id query rejects `joins` with a 400 (cross-entity reads there go + # through the composite-data plane). Everything else — including binnings + # and aggregates — uses the v3 by-id query, which also serves Federated + # entities and gates binning on the same `EnableBinningOnQuery` feature + # flag the v2 endpoint used. + if joins: endpoint = Endpoint( - f"datafabric_/api/v2/EntityService/entity/{entity_key}/query" + f"datafabric_/api/EntityService/entity/{entity_key}/query" ) else: endpoint = Endpoint( - f"datafabric_/api/EntityService/entity/{entity_key}/query" + f"datafabric_/api/v3/entities/entity/{entity_key}/query" ) return RequestSpec( diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entity_schema_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entity_schema_service.py index a73b894ec..1b96cf79c 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/_entity_schema_service.py +++ b/packages/uipath-platform/src/uipath/platform/entities/_entity_schema_service.py @@ -7,7 +7,7 @@ """ import re -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Sequence from httpx import Response @@ -19,11 +19,15 @@ from ..common._models import Endpoint, RequestSpec from ..orchestrator._folder_service import FolderService from .entities import ( + ENTITY_CLASS_TO_ID_MAP, ENTITY_FIELD_CONSTRAINT_DEFAULTS, ENTITY_FIELD_CONSTRAINT_SPEC, ENTITY_SCHEMA_FIELD_TYPE_MAP, RESERVED_FIELD_NAMES, Entity, + EntityClass, + EntityCreateExternalField, + EntityCreateExternalSource, EntityCreateFieldOptions, EntityCreateOptions, EntityFieldDataType, @@ -32,6 +36,11 @@ DATA_FABRIC_TENANT_FOLDER_ID = "00000000-0000-0000-0000-000000000000" +# v3 entities upsert endpoint (a full-definition create/replace). Federated +# creates require this endpoint (it accepts entityClassId / externalFields / +# sourceJoinConditionDetails). +_V3_ENTITIES = "datafabric_/api/v3/entities" + _NAME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9]*$") """Entity and field name pattern: must start with a letter, then letters and digits only. @@ -49,7 +58,8 @@ class EntitySchemaService(BaseService): """HTTP service for entity-schema operations. Provides retrieval and lifecycle management for entities and choice sets. - Backend target: ``datafabric_/api/Entity``. + Backend target: ``datafabric_/api/v3/entities`` (choice-set listing and + the by-name metadata read remain on the v1 ``datafabric_/api/Entity`` surface). See Also: https://docs.uipath.com/data-service/automation-cloud/latest/user-guide/introduction @@ -184,7 +194,7 @@ def _retrieve_spec(entity_key: str) -> RequestSpec: """Build the GET spec for fetching an entity by key.""" return RequestSpec( method="GET", - endpoint=Endpoint(f"datafabric_/api/Entity/{entity_key}"), + endpoint=Endpoint(f"{_V3_ENTITIES}/{entity_key}"), ) @staticmethod @@ -207,7 +217,7 @@ def _list_entities_spec() -> RequestSpec: """Build the GET spec for listing all entities (non-choice-sets).""" return RequestSpec( method="GET", - endpoint=Endpoint("datafabric_/api/Entity"), + endpoint=Endpoint(_V3_ENTITIES), ) @staticmethod @@ -225,39 +235,131 @@ def _create_entity_spec( fields: List[EntityCreateFieldOptions], options: Optional[EntityCreateOptions] = None, ) -> RequestSpec: - """Build the POST spec for creating an entity with its field schema.""" + """Build the POST spec for creating an entity with its field schema. + + Targets the v3 upsert endpoint. For a federated entity, pass + ``options.entity_class = EntityClass.Federated`` with at least one + source in ``options.external_fields`` and any cross-source joins in + ``options.source_join_condition_details``. + """ cls._validate_name(name, "entity") for field in fields: cls._validate_name(field.field_name, "field") opts = options or EntityCreateOptions() + + entity_class_id: Optional[int] = None + if opts.entity_class is not None: + if opts.entity_class not in (EntityClass.Native, EntityClass.Federated): + raise ValueError( + f"entityClass {opts.entity_class.value!r} is not creatable. " + "Use EntityClass.Native or EntityClass.Federated." + ) + entity_class_id = int(ENTITY_CLASS_TO_ID_MAP[opts.entity_class]) + if opts.entity_class is EntityClass.Federated and not opts.external_fields: + raise ValueError( + "Federated entities require at least one external source in " + "external_fields." + ) + # The user-facing option ``is_analytics_enabled`` maps to the legacy # backend field name ``isInsightsEnabled`` — the wire name predates # the "Analytics" UI rename. + entity_definition: Dict[str, Any] = { + "name": name, + "fields": [cls._build_schema_field_payload(f) for f in fields], + "folderId": opts.folder_key or DATA_FABRIC_TENANT_FOLDER_ID, + "isRbacEnabled": bool(opts.is_rbac_enabled or False), + "isInsightsEnabled": bool(opts.is_analytics_enabled or False), + "externalFields": cls._build_external_sources_payload(opts.external_fields), + } + if entity_class_id is not None: + entity_definition["entityClassId"] = entity_class_id + if opts.source_join_condition_details is not None: + entity_definition["sourceJoinConditionDetails"] = [ + j.model_dump(by_alias=True, exclude_none=True, mode="json") + for j in opts.source_join_condition_details + ] + payload: Dict[str, Any] = { "displayName": opts.display_name or name, - "entityDefinition": { - "name": name, - "fields": [cls._build_schema_field_payload(f) for f in fields], - "folderId": opts.folder_key or DATA_FABRIC_TENANT_FOLDER_ID, - "isRbacEnabled": bool(opts.is_rbac_enabled or False), - "isInsightsEnabled": bool(opts.is_analytics_enabled or False), - "externalFields": opts.external_fields or [], - }, + "entityDefinition": entity_definition, } if opts.description is not None: payload["description"] = opts.description return RequestSpec( method="POST", - endpoint=Endpoint("datafabric_/api/Entity"), + endpoint=Endpoint(_V3_ENTITIES), json=payload, ) + @classmethod + def _build_external_sources_payload( + cls, + sources: Optional[Sequence[EntityCreateExternalSource | Dict[str, Any]]], + ) -> List[Dict[str, Any]]: + """Build the wire ``externalFields`` payload for a federated entity. + + Each source's internal columns run through the same field pipeline as + native fields (so ``fieldDefinition`` is identical to a native field), + paired with its external mapping and source connection/object details. + Dict inputs are validated through :class:`EntityCreateExternalSource`. + """ + if not sources: + return [] + built: List[Dict[str, Any]] = [] + for source in sources: + if not isinstance(source, EntityCreateExternalSource): + source = EntityCreateExternalSource.model_validate(source) + entry: Dict[str, Any] = { + "fields": cls._build_external_fields_payload(source.fields), + "externalObjectDetail": source.external_object_detail.model_dump( + by_alias=True, exclude_none=True, mode="json" + ), + } + if source.external_connection_detail is not None: + entry["externalConnectionDetail"] = ( + source.external_connection_detail.model_dump( + by_alias=True, exclude_none=True, mode="json" + ) + ) + if source.native_connection_detail is not None: + entry["nativeConnectionDetail"] = ( + source.native_connection_detail.model_dump( + by_alias=True, exclude_none=True, mode="json" + ) + ) + built.append(entry) + return built + + @classmethod + def _build_external_fields_payload( + cls, + fields: Optional[List[EntityCreateExternalField]], + ) -> List[Dict[str, Any]]: + """Build the wire ``fields`` payload for a federated source. + + Produces ``{fieldDefinition, externalFieldMappingDetail}`` per field — + ``fieldDefinition`` is the native field payload, ``externalFieldMappingDetail`` + the source mapping (``directionType`` numeric, per :class:`DataDirectionType`). + """ + if not fields: + return [] + return [ + { + "fieldDefinition": cls._build_schema_field_payload(f.field), + "externalFieldMappingDetail": f.external_field_mapping_detail.model_dump( + by_alias=True, exclude_none=True, mode="json" + ), + } + for f in fields + ] + @staticmethod def _delete_entity_spec(entity_id: str) -> RequestSpec: """Build the DELETE spec for removing an entity.""" return RequestSpec( method="DELETE", - endpoint=Endpoint(f"datafabric_/api/Entity/{entity_id}"), + endpoint=Endpoint(f"{_V3_ENTITIES}/{entity_id}"), ) @staticmethod @@ -277,7 +379,7 @@ def _update_entity_metadata_spec( body = metadata.model_dump(by_alias=True, exclude_none=True) return RequestSpec( method="PATCH", - endpoint=Endpoint(f"datafabric_/api/Entity/{entity_id}/metadata"), + endpoint=Endpoint(f"{_V3_ENTITIES}/{entity_id}/metadata"), json=body, ) diff --git a/packages/uipath-platform/src/uipath/platform/entities/entities.py b/packages/uipath-platform/src/uipath/platform/entities/entities.py index 51eea2d1b..44b76e9d4 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/entities.py +++ b/packages/uipath-platform/src/uipath/platform/entities/entities.py @@ -48,17 +48,37 @@ class FieldDisplayType(Enum): AutoNumber = "AutoNumber" -class DataDirectionType(Enum): - """Enum representing data direction types for fields in entities.""" +class DataDirectionType(IntEnum): + """Data direction for an external field mapping. - ReadOnly = "ReadOnly" - ReadAndWrite = "ReadAndWrite" + Numeric on the wire — the string form (e.g. ``"ReadOnly"``) is rejected by + the v3 create/upsert endpoint with a 500. ``ReadOnly`` is read-only, + ``ReadAndWrite`` supports reads and writes. + + For backward compatibility, the legacy string member names (``"ReadOnly"`` / + ``"ReadAndWrite"``, case-insensitive) are still accepted as input — e.g. from + older persisted payloads — and normalise to the numeric members. + """ + + ReadOnly = 0 + ReadAndWrite = 1 + + @classmethod + def _missing_(cls, value: object) -> "DataDirectionType | None": + """Accept legacy string member names as input, mapping them to the enum.""" + if isinstance(value, str): + key = value.strip().lower() + for member in cls: + if member.name.lower() == key: + return member + return None -class JoinType(Enum): +class JoinType(str, Enum): """Enum representing types of joins between entities.""" LeftJoin = "LeftJoin" + InnerJoin = "InnerJoin" class EntityType(Enum): @@ -70,6 +90,33 @@ class EntityType(Enum): SystemEntity = "SystemEntity" +class EntityClass(str, Enum): + """Product class of a Data Fabric entity. + + ``Native`` stores its data in UiPath. ``Federated`` is an experimental, + read-only unified view over one or more external / native sources. ``Case`` + is read-only and only ever returned by reads (never creatable). + """ + + Native = "Native" + Federated = "Federated" + Case = "Case" + + +class EntityClassId(IntEnum): + """Internal numeric discriminator for :class:`EntityClass` on the wire.""" + + Native = 9 + Federated = 10 + + +ENTITY_CLASS_TO_ID_MAP: Dict[EntityClass, EntityClassId] = { + EntityClass.Native: EntityClassId.Native, + EntityClass.Federated: EntityClassId.Federated, +} +"""Maps a public :class:`EntityClass` to its wire :class:`EntityClassId`.""" + + class EntityFieldMetadata(BaseModel): """Model representing metadata for an entity field.""" @@ -766,6 +813,156 @@ class EntityCreateFieldOptions(BaseModel): ) +# -------------------------------------------------------------------------- +# Federated entity create/update types (experimental) +# -------------------------------------------------------------------------- + + +class SearchabilityOperator(BaseModel): + """Operator-based searchability for a federated external field mapping.""" + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + searchable_operators: Optional[List[str]] = Field( + default=None, alias="searchableOperators" + ) + + +class SearchabilityNamedSearch(BaseModel): + """Named-search searchability for a federated external field mapping.""" + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + searchable_names: Optional[List[str]] = Field(default=None, alias="searchableNames") + + +class Searchability(BaseModel): + """Searchability metadata attached to a federated external field mapping.""" + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + searchable: bool + supports_operators: Optional[SearchabilityOperator] = Field( + default=None, alias="supportsOperators" + ) + supports_named_search: Optional[SearchabilityNamedSearch] = Field( + default=None, alias="supportsNamedSearch" + ) + + +class EntityCreateExternalConnection(BaseModel): + """Integration Service connection backing a federated external source.""" + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + connection_id: str = Field(alias="connectionId") + element_instance_id: Optional[int] = Field(default=None, alias="elementInstanceId") + folder_key: Optional[str] = Field(default=None, alias="folderKey") + connector_key: str = Field(alias="connectorKey") + connector_name: str = Field(alias="connectorName") + connection_name: Optional[str] = Field(default=None, alias="connectionName") + + +class NativeConnectionDetail(BaseModel): + """Reference to another UiPath entity used as a federated native source.""" + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + entity_id: str = Field(alias="entityId") + folder_key: str = Field(alias="folderKey") + + +class EntityCreateExternalObject(BaseModel): + """Identity of the object a federated source reads from.""" + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + external_object_name: str = Field(alias="externalObjectName") + external_object_display_name: Optional[str] = Field( + default=None, alias="externalObjectDisplayName" + ) + primary_key: Optional[str] = Field(default=None, alias="primaryKey") + is_primary_source: Optional[bool] = Field(default=None, alias="isPrimarySource") + method: Optional[str] = Field(default=None, alias="method") + + +class EntityCreateExternalFieldMapping(BaseModel): + """Maps an internal column to a field on the external source. + + ``direction_type`` is numeric on the wire (see :class:`DataDirectionType`). + """ + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + external_field_name: str = Field(alias="externalFieldName") + external_field_display_name: Optional[str] = Field( + default=None, alias="externalFieldDisplayName" + ) + external_field_type: Optional[str] = Field(default=None, alias="externalFieldType") + direction_type: DataDirectionType = Field( + default=DataDirectionType.ReadOnly, alias="directionType" + ) + searchability: Optional[Searchability] = Field(default=None, alias="searchability") + is_required_for_read: Optional[bool] = Field( + default=None, alias="isRequiredForRead" + ) + sortable: Optional[bool] = Field(default=None, alias="sortable") + + +class EntityCreateExternalField(BaseModel): + """A field a federated source contributes: internal column + external mapping.""" + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + field: EntityCreateFieldOptions + external_field_mapping_detail: EntityCreateExternalFieldMapping = Field( + alias="externalFieldMappingDetail" + ) + + +class EntityCreateExternalSource(BaseModel): + """A single source on a federated entity (connector object or native entity). + + Use ``external_connection_detail`` for an Integration Service connector + source, or ``native_connection_detail`` for another UiPath entity. + """ + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + fields: Optional[List[EntityCreateExternalField]] = Field(default=None) + external_object_detail: EntityCreateExternalObject = Field( + alias="externalObjectDetail" + ) + external_connection_detail: Optional[EntityCreateExternalConnection] = Field( + default=None, alias="externalConnectionDetail" + ) + native_connection_detail: Optional[NativeConnectionDetail] = Field( + default=None, alias="nativeConnectionDetail" + ) + + +class SourceJoinConditionDetail(BaseModel): + """Write-shape join between two federated sources (names + connection ids). + + Each side's connection id is the Integration Service ``connectionId`` for a + connector source, or the DF entity id for a native source. + """ + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + source_object_name: str = Field(alias="sourceObjectName") + source_join_field: str = Field(alias="sourceJoinField") + source_object_connection_id: Optional[str] = Field( + default=None, alias="sourceObjectConnectionId" + ) + join_type: JoinType = Field(default=JoinType.LeftJoin, alias="joinType") + related_source_object_name: str = Field(alias="relatedSourceObjectName") + related_source_join_field: str = Field(alias="relatedSourceJoinField") + related_source_object_connection_id: Optional[str] = Field( + default=None, alias="relatedSourceObjectConnectionId" + ) + + class EntityCreateOptions(BaseModel): """Options for creating a new Data Fabric entity.""" @@ -780,8 +977,12 @@ class EntityCreateOptions(BaseModel): is_analytics_enabled: Optional[bool] = Field( default=None, alias="isAnalyticsEnabled" ) - external_fields: Optional[List[Dict[str, Any]]] = Field( - default=None, alias="externalFields" + entity_class: Optional[EntityClass] = Field(default=None, alias="entityClass") + external_fields: Optional[List[EntityCreateExternalSource | Dict[str, Any]]] = ( + Field(default=None, alias="externalFields") + ) + source_join_condition_details: Optional[List[SourceJoinConditionDetail]] = Field( + default=None, alias="sourceJoinConditionDetails" ) diff --git a/packages/uipath-platform/tests/services/test_entities_service.py b/packages/uipath-platform/tests/services/test_entities_service.py index 9a63bbc46..1a3405f4a 100644 --- a/packages/uipath-platform/tests/services/test_entities_service.py +++ b/packages/uipath-platform/tests/services/test_entities_service.py @@ -93,7 +93,7 @@ def test_retrieve( ) -> None: entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/{entity_key}", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/{entity_key}", status_code=200, json={ "name": "TestEntity", @@ -147,7 +147,7 @@ def test_retrieve( assert sent_request.method == "GET" assert ( sent_request.url - == f"{base_url}{org}{tenant}/datafabric_/api/Entity/{entity_key}" + == f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/{entity_key}" ) def test_retrieve_records_with_no_schema_succeeds( @@ -161,7 +161,7 @@ def test_retrieve_records_with_no_schema_succeeds( ) -> None: entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{str(entity_key)}/read?start=0&limit=1", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{str(entity_key)}/read?start=0&limit=1", status_code=200, json={ "totalCount": 1, @@ -199,7 +199,7 @@ def test_retrieve_records_with_schema_succeeds( ) -> None: entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{str(entity_key)}/read?start=0&limit=1", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{str(entity_key)}/read?start=0&limit=1", status_code=200, json={ "totalCount": 1, @@ -250,7 +250,7 @@ def test_retrieve_records_with_optional_fields( ) -> None: entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{str(entity_key)}/read?start=0&limit=1", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{str(entity_key)}/read?start=0&limit=1", status_code=200, json={ "totalCount": 1, @@ -308,7 +308,7 @@ def test_retrieve_records_without_start_and_limit( ) -> None: entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{str(entity_key)}/read", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{str(entity_key)}/read", status_code=200, json={ "totalCount": 1, @@ -1199,7 +1199,7 @@ def test_insert_record_fires_post_with_expansion_level( entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/insert?expansionLevel=2", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/insert?expansionLevel=2", status_code=200, json={"Id": "rec-1", "name": "alice"}, ) @@ -1229,7 +1229,7 @@ async def test_insert_record_async( ) -> None: entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/insert", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/insert", status_code=200, json={"Id": "rec-1"}, ) @@ -1251,7 +1251,7 @@ def test_get_record( entity_key = uuid.uuid4() record_id = "12345" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/read/{record_id}?expansionLevel=1", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/read/{record_id}?expansionLevel=1", status_code=200, json={"Id": record_id, "name": "found"}, ) @@ -1274,7 +1274,7 @@ def test_update_record_accepts_dict( entity_key = uuid.uuid4() record_id = "rec-9" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update/{record_id}", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/update/{record_id}", status_code=200, json={"Id": record_id, "name": "updated"}, ) @@ -1302,7 +1302,7 @@ def test_delete_record_uses_http_delete( entity_key = uuid.uuid4() record_id = "rec-9" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/delete/{record_id}", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/delete/{record_id}", method="DELETE", status_code=200, ) @@ -1313,7 +1313,7 @@ def test_delete_record_uses_http_delete( assert sent is not None assert sent.method == "DELETE" - def test_query_v1_with_filter_and_pagination( + def test_query_v3_by_id_with_filter_and_pagination( self, httpx_mock: HTTPXMock, service: EntitiesService, @@ -1333,7 +1333,7 @@ def test_query_v1_with_filter_and_pagination( entity_key = uuid.uuid4() httpx_mock.add_response( url=re.compile( - rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/query.*" + rf"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/query.*" ), status_code=200, json={ @@ -1401,7 +1401,7 @@ def test_query_aggregate_response_handles_id_less_rows( entity_key = uuid.uuid4() httpx_mock.add_response( url=re.compile( - rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/query.*" + rf"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/query.*" ), status_code=200, json={ @@ -1440,7 +1440,7 @@ def test_query_aggregate_response_handles_id_less_rows( assert body["aggregates"][0]["alias"] == "total" assert body["groupBy"] == ["status"] - def test_query_v2_when_binnings_provided( + def test_query_binnings_route_to_v3( self, httpx_mock: HTTPXMock, service: EntitiesService, @@ -1449,15 +1449,16 @@ def test_query_v2_when_binnings_provided( tenant: str, version: str, ) -> None: + """Binnings go to the v3 by-id query (which supports them, FF-gated), not v2.""" from uipath.platform.entities import EntityAggregateFunction, EntityBinning entity_key = uuid.uuid4() httpx_mock.add_response( url=re.compile( - rf"{base_url}{org}{tenant}/datafabric_/api/v2/EntityService/entity/{entity_key}/query.*" + rf"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/query.*" ), status_code=200, - json={"value": [], "totalCount": 0}, + json={"value": [], "totalRecordCount": 0}, ) service.retrieve_records( @@ -1473,7 +1474,52 @@ def test_query_v2_when_binnings_provided( sent = httpx_mock.get_request() assert sent is not None - assert "/v2/EntityService/" in str(sent.url) + assert "/v3/entities/entity/" in str(sent.url) + assert "/v2/" not in str(sent.url) + body = json.loads(sent.content) + assert body["binnings"][0]["fieldName"] == "status" + + def test_query_with_joins_routes_to_v1( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + """Multi-entity joins stay on the v1 by-key endpoint (v3 by-id rejects joins).""" + from uipath.platform.entities import EntityJoin + + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=re.compile( + rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/query.*" + ), + status_code=200, + json={"value": [], "totalRecordCount": 0}, + ) + + service.retrieve_records( + entity_key=str(entity_key), + joins=[ + EntityJoin( + entity_name="Order", + join_type="LeftJoin", + join_field_name="customerId", + related_entity_name="Customer", + related_field_name="Id", + ) + ], + ) + + sent = httpx_mock.get_request() + assert sent is not None + # v1 by-key path — not the v3 by-id path. + assert "/api/EntityService/entity/" in str(sent.url) + assert "/v3/" not in str(sent.url) + body = json.loads(sent.content) + assert body["joins"][0]["entityName"] == "Order" def test_upload_attachment_sends_multipart( self, @@ -1573,7 +1619,7 @@ def test_create_entity_returns_id( new_entity_id = str(uuid.uuid4()) httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/Entity", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities", method="POST", status_code=200, json=new_entity_id, @@ -1605,6 +1651,143 @@ def test_create_entity_returns_id( assert body["entityDefinition"]["fields"][0]["name"] == "productName" assert body["entityDefinition"]["isRbacEnabled"] is True + def test_create_federated_entity_builds_v3_payload( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + from uipath.platform.entities import ( + EntityClass, + EntityCreateExternalConnection, + EntityCreateExternalField, + EntityCreateExternalFieldMapping, + EntityCreateExternalObject, + EntityCreateExternalSource, + EntityCreateFieldOptions, + EntityCreateOptions, + NativeConnectionDetail, + SourceJoinConditionDetail, + ) + + new_entity_id = str(uuid.uuid4()) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities", + method="POST", + status_code=200, + json=new_entity_id, + ) + + connector_source = EntityCreateExternalSource( + external_object_detail=EntityCreateExternalObject( + external_object_name="sys_user", + primary_key="sys_id", + is_primary_source=True, + method="{}", + ), + external_connection_detail=EntityCreateExternalConnection( + connection_id="conn-1", + element_instance_id=373410, + connector_key="uipath-servicenow-servicenow", + connector_name="ServiceNow", + ), + fields=[ + EntityCreateExternalField( + field=EntityCreateFieldOptions(field_name="UserName"), + external_field_mapping_detail=EntityCreateExternalFieldMapping( + external_field_name="user_name" + ), + ) + ], + ) + native_source = EntityCreateExternalSource( + external_object_detail=EntityCreateExternalObject( + external_object_name="UserDept", primary_key="Id" + ), + native_connection_detail=NativeConnectionDetail( + entity_id="dept-entity-id", folder_key="folder-1" + ), + fields=[ + EntityCreateExternalField( + field=EntityCreateFieldOptions(field_name="Department"), + external_field_mapping_detail=EntityCreateExternalFieldMapping( + external_field_name="Department" + ), + ) + ], + ) + + created_id = service.create_entity( + name="UserDirectory", + fields=[], + options=EntityCreateOptions( + entity_class=EntityClass.Federated, + external_fields=[connector_source, native_source], + source_join_condition_details=[ + SourceJoinConditionDetail( + source_object_name="sys_user", + source_join_field="user_name", + source_object_connection_id="conn-1", + related_source_object_name="UserDept", + related_source_join_field="UserNameDept", + related_source_object_connection_id="dept-entity-id", + ) + ], + ), + ) + + assert created_id == new_entity_id + sent = httpx_mock.get_request() + assert sent is not None + definition = json.loads(sent.content)["entityDefinition"] + # Federated class discriminator (numeric wire id). + assert definition["entityClassId"] == 10 + # Two sources; the connector source's field wraps in `fieldDefinition` + # and its mapping uses the numeric directionType. + sources = definition["externalFields"] + assert len(sources) == 2 + connector = sources[0] + assert connector["externalObjectDetail"]["externalObjectName"] == "sys_user" + assert connector["externalConnectionDetail"]["elementInstanceId"] == 373410 + field = connector["fields"][0] + assert field["fieldDefinition"]["name"] == "UserName" + assert field["externalFieldMappingDetail"]["externalFieldName"] == "user_name" + assert field["externalFieldMappingDetail"]["directionType"] == 0 + # Native source carries its entity reference, not a connector connection. + assert sources[1]["nativeConnectionDetail"]["entityId"] == "dept-entity-id" + # Join is passed through in write shape. + join = definition["sourceJoinConditionDetails"][0] + assert join["sourceObjectName"] == "sys_user" + assert join["relatedSourceObjectName"] == "UserDept" + assert join["joinType"] == "LeftJoin" + + def test_create_federated_entity_requires_sources( + self, service: EntitiesService + ) -> None: + from uipath.platform.entities import EntityClass, EntityCreateOptions + + with pytest.raises(ValueError, match="at least one external source"): + service.create_entity( + name="EmptyFederated", + fields=[], + options=EntityCreateOptions(entity_class=EntityClass.Federated), + ) + + def test_create_entity_rejects_uncreatable_class( + self, service: EntitiesService + ) -> None: + from uipath.platform.entities import EntityClass, EntityCreateOptions + + with pytest.raises(ValueError, match="is not creatable"): + service.create_entity( + name="CaseEntity", + fields=[], + options=EntityCreateOptions(entity_class=EntityClass.Case), + ) + def test_delete_entity( self, httpx_mock: HTTPXMock, @@ -1616,7 +1799,7 @@ def test_delete_entity( ) -> None: entity_id = "ent-doomed" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/{entity_id}", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/{entity_id}", method="DELETE", status_code=200, ) @@ -1639,7 +1822,7 @@ def test_update_entity_metadata( entity_id = "ent-meta" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/{entity_id}/metadata", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/{entity_id}/metadata", method="PATCH", status_code=200, json={}, @@ -1695,7 +1878,7 @@ def test_list_records_returns_paginated_metadata( entity_key = uuid.uuid4() httpx_mock.add_response( url=re.compile( - rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/read.*" + rf"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/read.*" ), status_code=200, json={ @@ -1747,7 +1930,7 @@ def test_insert_records_passes_expansion_level_and_fail_on_first( entity_key = uuid.uuid4() httpx_mock.add_response( url=re.compile( - rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/insert-batch.*" + rf"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/insert-batch.*" ), status_code=200, json={"successRecords": [{"Id": "1"}], "failureRecords": []}, @@ -1785,7 +1968,7 @@ def test_update_records_recovers_failure_records_from_4xx( """ entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update-batch", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/update-batch", method="POST", status_code=400, json={ @@ -1815,7 +1998,7 @@ def test_delete_records_recovers_failure_records_from_4xx( ) -> None: entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/delete-batch", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/delete-batch", method="POST", status_code=400, json={ @@ -1865,7 +2048,7 @@ def _captured_field( from uipath.platform.entities import EntityCreateOptions httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/Entity", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities", method="POST", status_code=200, json="00000000-0000-0000-0000-000000000001", @@ -2112,6 +2295,26 @@ def test_create_entity_rejects_relationship_without_reference_entity( ], ) + def test_data_direction_type_accepts_legacy_string_names(self) -> None: + """Legacy string member names normalise to the numeric members (back-compat).""" + from uipath.platform.entities import ( + DataDirectionType, + EntityCreateExternalFieldMapping, + ) + + # Legacy string input is intentionally off-type (the field expects the + # numeric enum); `_missing_` normalises it, so silence the type checker. + assert DataDirectionType("ReadOnly") is DataDirectionType.ReadOnly # type: ignore[arg-type] + assert DataDirectionType("readandwrite") is DataDirectionType.ReadAndWrite # type: ignore[arg-type] + assert int(DataDirectionType.ReadOnly) == 0 + # A mapping built from a legacy string serialises to the numeric wire form. + mapping = EntityCreateExternalFieldMapping( + external_field_name="user_name", + direction_type="ReadOnly", # type: ignore[arg-type] + ) + dumped = mapping.model_dump(by_alias=True, exclude_none=True, mode="json") + assert dumped["directionType"] == 0 + def test_entity_query_filter_rejects_in_without_value_list(self) -> None: from uipath.platform.entities import EntityQueryFilter, QueryFilterOperator @@ -2178,7 +2381,7 @@ async def test_get_record_async( entity_key = uuid.uuid4() record_id = "rec-1" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/read/{record_id}", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/read/{record_id}", status_code=200, json={"Id": record_id, "name": "found"}, ) @@ -2187,13 +2390,13 @@ async def test_get_record_async( ) assert record.id == record_id - async def test_query_async_v1( + async def test_query_async_v3_by_id( self, httpx_mock, service, base_url, org, tenant, version ) -> None: entity_key = uuid.uuid4() httpx_mock.add_response( url=re.compile( - rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/query" + rf"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/query" ), status_code=200, json={"value": [{"Id": "1"}], "totalRecordCount": 1}, @@ -2207,7 +2410,7 @@ async def test_delete_record_async( entity_key = uuid.uuid4() record_id = "rec-1" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/delete/{record_id}", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/delete/{record_id}", method="DELETE", status_code=200, ) @@ -2225,7 +2428,7 @@ async def test_create_entity_async( new_id = "00000000-0000-0000-0000-000000000123" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/Entity", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities", method="POST", status_code=200, json=new_id, @@ -2244,7 +2447,7 @@ async def test_delete_entity_async( self, httpx_mock, service, base_url, org, tenant, version ) -> None: httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/ent-1", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/ent-1", method="DELETE", status_code=200, ) @@ -2254,7 +2457,7 @@ async def test_update_entity_metadata_async_with_dict( self, httpx_mock, service, base_url, org, tenant, version ) -> None: httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/ent-1/metadata", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/ent-1/metadata", method="PATCH", status_code=200, json={}, @@ -2272,7 +2475,7 @@ def test_update_entity_metadata_normalizes_snake_case_dict_keys( ) -> None: """Snake_case dict keys must be sent to the backend as camelCase.""" httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/ent-1/metadata", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/ent-1/metadata", method="PATCH", status_code=200, json={}, @@ -2352,7 +2555,7 @@ def test_4xx_recovery_only_400_with_strict_shape( entity_key = uuid.uuid4() # 500 with the shape — must propagate, not be silently treated as success. httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update-batch", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/update-batch", method="POST", status_code=500, json={"successRecords": [], "failureRecords": []}, @@ -2370,7 +2573,7 @@ def test_4xx_recovery_404_propagates( entity_key = uuid.uuid4() # 404 with valid shape — still propagates because not a 400. httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update-batch", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/update-batch", method="POST", status_code=404, json={"successRecords": [], "failureRecords": []}, @@ -2389,7 +2592,7 @@ def test_4xx_recovery_400_unrelated_body_propagates( must surface as an exception (so generic validation errors aren't masked).""" entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update-batch", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/update-batch", method="POST", status_code=400, json={"error": "Validation failed", "code": "InvalidArg"}, @@ -2416,7 +2619,7 @@ async def test_retrieve_async( ) -> None: entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/{entity_key}", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/{entity_key}", status_code=200, json={ "name": "Customers", @@ -2491,7 +2694,7 @@ def test_list_entities_basic( version: str, ) -> None: httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/Entity", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities", status_code=200, json=[ { @@ -2525,7 +2728,7 @@ async def test_list_entities_async( version: str, ) -> None: httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/Entity", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities", status_code=200, json=[ { @@ -2553,7 +2756,7 @@ async def test_list_records_async( entity_key = uuid.uuid4() httpx_mock.add_response( url=re.compile( - rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/read.*" + rf"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/read.*" ), status_code=200, json={ @@ -2577,7 +2780,7 @@ async def test_update_record_async( ) -> None: entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update/rec-1", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/update/rec-1", method="POST", status_code=200, json={"Id": "rec-1", "name": "renamed"}, @@ -2601,7 +2804,7 @@ async def test_insert_records_async_batch( entity_key = uuid.uuid4() httpx_mock.add_response( url=re.compile( - rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/insert-batch.*" + rf"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/insert-batch.*" ), status_code=200, json={ @@ -2628,7 +2831,7 @@ async def test_update_records_async_recovers_400_failures( ) -> None: entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update-batch", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/update-batch", method="POST", status_code=400, json={ @@ -2654,7 +2857,7 @@ async def test_delete_records_async_batch( entity_key = uuid.uuid4() httpx_mock.add_response( url=re.compile( - rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/delete-batch.*" + rf"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/delete-batch.*" ), status_code=200, json={ @@ -2721,7 +2924,7 @@ def test_5xx_with_batch_shape_still_propagates( entity_key = uuid.uuid4() httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/insert-batch", + url=f"{base_url}{org}{tenant}/datafabric_/api/v3/entities/entity/{entity_key}/insert-batch", method="POST", status_code=500, json={"successRecords": [], "failureRecords": []}, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 2e506b0af..ef07df575 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.17" +version = "0.2.18" source = { editable = "." } dependencies = [ { name = "anyio" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 13c6bacff..f9c60b86a 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2760,7 +2760,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.2.17" +version = "0.2.18" source = { editable = "../uipath-platform" } dependencies = [ { name = "anyio" },