Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
28 changes: 28 additions & 0 deletions packages/uipath-platform/src/uipath/platform/entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@
from .entities import (
AggregateRow,
ChoiceSetValue,
DataDirectionType,
DataFabricEntityItem,
Entity,
EntityAggregate,
EntityAggregateFunction,
EntityBinning,
EntityClass,
EntityClassId,
EntityCreateExternalConnection,
EntityCreateExternalField,
Comment on lines 14 to +20
EntityCreateExternalFieldMapping,
EntityCreateExternalObject,
EntityCreateExternalSource,
EntityCreateFieldOptions,
EntityCreateOptions,
EntityField,
Expand All @@ -35,24 +43,38 @@
FailureRecord,
FieldDataType,
FieldMetadata,
JoinType,
LogicalOperator,
NativeConnectionDetail,
QueryFilterOperator,
QueryRoutingOverrideContext,
ReferenceType,
RetrieveEntityRecordsResponse,
Searchability,
SearchabilityNamedSearch,
SearchabilityOperator,
SourceJoinConditionDetail,
SourceJoinCriteria,
)

__all__ = [
"AggregateRow",
"ChoiceSetValue",
"DataDirectionType",
"DataFabricEntityItem",
"DataFabricOntologyItem",
"EntitiesService",
"Entity",
"EntityAggregate",
"EntityAggregateFunction",
"EntityBinning",
"EntityClass",
"EntityClassId",
"EntityCreateExternalConnection",
"EntityCreateExternalField",
"EntityCreateExternalFieldMapping",
"EntityCreateExternalObject",
"EntityCreateExternalSource",
"EntityCreateFieldOptions",
"EntityCreateOptions",
"EntityField",
Expand All @@ -75,10 +97,16 @@
"FailureRecord",
"FieldDataType",
"FieldMetadata",
"JoinType",
"LogicalOperator",
"NativeConnectionDetail",
"QueryFilterOperator",
"QueryRoutingOverrideContext",
"ReferenceType",
"RetrieveEntityRecordsResponse",
"Searchability",
"SearchabilityNamedSearch",
"SearchabilityOperator",
"SourceJoinConditionDetail",
"SourceJoinCriteria",
]
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand All @@ -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),
Expand All @@ -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,
)
Expand All @@ -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),
Expand All @@ -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}"
),
)

Expand All @@ -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],
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand All @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
)

Expand Down
Loading
Loading