Skip to content

Commit 0d647c0

Browse files
cezara98tclaude
andcommitted
feat(platform): add IXP designtime projects service [ACTV-89365]
First resource-service port on top of the IXP designtime transport (Step 2 of the IXP -> Coded Agents SDK effort). Establishes the template the remaining designtime services follow. - ProjectsService(IxpDesigntimeService): list / retrieve / create / update (title) / delete, each with an async twin. Writes go through the transport's no-retry helpers; list/retrieve keep the platform retry policy. - Pydantic models (Project, ProjectsPage, DeleteProjectResponse): idiomatic snake_case attributes aliased to the API's PascalCase wire keys, CreatedAt parsed to datetime; extra="allow" for forward compatibility. - DocumentProjectsService facade grouping resource services under properties that mirror the CLI command groups (sdk.document_projects.projects.*), wired onto the UiPath client as `document_projects`. - 11 pytest-httpx tests (url/api-version/paging, PascalCase body + snake_case model mapping, name percent-encoding, delete envelope, facade caching, async twins). ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 26ccf11 commit 0d647c0

6 files changed

Lines changed: 508 additions & 3 deletions

File tree

packages/uipath-platform/src/uipath/platform/_uipath.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .common.auth import resolve_config_from_env
1919
from .connections import ConnectionsService
2020
from .context_grounding import ContextGroundingService
21+
from .document_projects import DocumentProjectsService
2122
from .documents import DocumentsService
2223
from .entities import EntitiesService
2324
from .errors import BaseUrlMissingError, SecretMissingError
@@ -126,6 +127,10 @@ def memory(self) -> MemoryService:
126127
def documents(self) -> DocumentsService:
127128
return DocumentsService(self._config, self._execution_context)
128129

130+
@property
131+
def document_projects(self) -> DocumentProjectsService:
132+
return DocumentProjectsService(self._config, self._execution_context)
133+
129134
@property
130135
def queues(self) -> QueuesService:
131136
return QueuesService(self._config, self._execution_context)

packages/uipath-platform/src/uipath/platform/document_projects/__init__.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
"""UiPath IXP design-time SDK (``du_/api/designtimeapi``).
22
3-
This package hosts the IXP design-time services (projects, taxonomy, labellings,
4-
documents, models). It currently provides the shared transport foundation;
5-
resource services are added on top of :class:`IxpDesigntimeService`.
3+
This package hosts the IXP design-time services (projects, and — as they land —
4+
fields, data-types, groups, documents, labellings, models). Reach the surface via
5+
``sdk.document_projects``; resource services build on the shared transport
6+
foundation :class:`IxpDesigntimeService`.
67
"""
78

9+
from ._document_projects_service import DocumentProjectsService
10+
from ._models import DeleteProjectResponse, Project, ProjectsPage
11+
from ._projects_service import ProjectsService
812
from ._transport import (
913
DESIGNTIME_API_BASE,
1014
DESIGNTIME_API_VERSION,
1115
IxpDesigntimeService,
1216
)
1317

1418
__all__ = [
19+
"DocumentProjectsService",
20+
"ProjectsService",
21+
"Project",
22+
"ProjectsPage",
23+
"DeleteProjectResponse",
1524
"IxpDesigntimeService",
1625
"DESIGNTIME_API_BASE",
1726
"DESIGNTIME_API_VERSION",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Public facade for the IXP design-time surface (``du_/api/designtimeapi``).
2+
3+
:class:`DocumentProjectsService` is the entry point reached via
4+
``sdk.document_projects``. It groups the design-time resource services under
5+
properties that mirror the CLI's command groups (``uip ixp projects``,
6+
``uip ixp fields``, ...), so callers write ``sdk.document_projects.projects.list()``.
7+
8+
Only the ``projects`` resource is wired today; sibling resources (fields,
9+
data-types, groups, documents, labellings, deployments) are added as their
10+
services land, each as a new property here.
11+
"""
12+
13+
from functools import cached_property
14+
15+
from ..common._config import UiPathApiConfig
16+
from ..common._execution_context import UiPathExecutionContext
17+
from ._projects_service import ProjectsService
18+
19+
20+
class DocumentProjectsService:
21+
"""Grouped access to the IXP design-time resource services.
22+
23+
!!! warning "Preview Feature"
24+
This service is experimental. Behavior and parameters may change in
25+
future versions.
26+
"""
27+
28+
def __init__(
29+
self,
30+
config: UiPathApiConfig,
31+
execution_context: UiPathExecutionContext,
32+
) -> None:
33+
self._config = config
34+
self._execution_context = execution_context
35+
36+
@cached_property
37+
def projects(self) -> ProjectsService:
38+
"""The projects resource — create, list, retrieve, update, delete."""
39+
return ProjectsService(
40+
config=self._config, execution_context=self._execution_context
41+
)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Pydantic models for the IXP design-time projects resource.
2+
3+
The design-time API serializes with PascalCase keys (``Id``, ``Name``, ...); these
4+
models expose idiomatic snake_case attributes and map to the wire names via field
5+
aliases, so callers work with native Python objects (``project.created_at``) rather
6+
than the raw envelope. ``extra="allow"`` keeps forward compatibility if the API
7+
grows fields before this SDK models them.
8+
"""
9+
10+
from datetime import datetime
11+
12+
from pydantic import BaseModel, ConfigDict, Field
13+
14+
15+
class Project(BaseModel):
16+
"""A design-time project (``GET``/``POST``/``PATCH /api/projects``).
17+
18+
Mirrors the design-time API ``Project`` contract. ``name`` is the backend slug
19+
(unique within the tenant) used to address the project in every other call;
20+
``title`` is the human-readable display name.
21+
"""
22+
23+
model_config = ConfigDict(
24+
validate_by_name=True,
25+
validate_by_alias=True,
26+
extra="allow",
27+
)
28+
29+
id: str = Field(alias="Id")
30+
name: str = Field(alias="Name")
31+
title: str = Field(alias="Title")
32+
created_at: datetime = Field(alias="CreatedAt")
33+
34+
35+
class ProjectsPage(BaseModel):
36+
"""A page of projects (``GET /api/projects``).
37+
38+
``total`` is the full project count regardless of the window; ``offset`` and
39+
``limit`` echo the applied paging window.
40+
"""
41+
42+
model_config = ConfigDict(
43+
validate_by_name=True,
44+
validate_by_alias=True,
45+
extra="allow",
46+
)
47+
48+
projects: list[Project] = Field(alias="Projects")
49+
total: int = Field(alias="Total")
50+
offset: int = Field(alias="Offset")
51+
limit: int = Field(alias="Limit")
52+
53+
54+
class DeleteProjectResponse(BaseModel):
55+
"""Result of deleting a project (``DELETE /api/projects/{projectName}``).
56+
57+
A minimal success confirmation; ``status`` is ``"ok"`` on success.
58+
"""
59+
60+
model_config = ConfigDict(
61+
validate_by_name=True,
62+
validate_by_alias=True,
63+
extra="allow",
64+
)
65+
66+
status: str = Field(alias="Status")
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
"""Design-time projects resource service.
2+
3+
:class:`ProjectsService` covers the project lifecycle exposed by the CLI's
4+
``uip ixp projects`` group core verbs — list, retrieve, create, update (title),
5+
delete — over ``du_/api/designtimeapi``. Sub-resources of a project (fields,
6+
data-types, taxonomy, documents, models, ...) are handled by sibling services.
7+
8+
It builds on :class:`IxpDesigntimeService`, so writes (create/update/delete) are
9+
**not** retried while list/retrieve keep the platform retry policy.
10+
"""
11+
12+
from uipath.core.tracing import traced
13+
14+
from ._models import DeleteProjectResponse, Project, ProjectsPage
15+
from ._transport import IxpDesigntimeService
16+
17+
#: Default project-list page size — matches the design-time API's server-side default.
18+
DEFAULT_LIST_LIMIT = 50
19+
20+
_PROJECTS = "/api/projects"
21+
_PROJECT = "/api/projects/{name}"
22+
23+
24+
class ProjectsService(IxpDesigntimeService):
25+
"""Manage IXP design-time projects.
26+
27+
Accessed as ``sdk.document_projects.projects``. Each method has an async twin
28+
with the ``_async`` suffix.
29+
"""
30+
31+
@traced(name="projects_list", run_type="uipath")
32+
def list(self, *, offset: int = 0, limit: int = DEFAULT_LIST_LIMIT) -> ProjectsPage:
33+
"""List projects visible to the caller.
34+
35+
Args:
36+
offset: Number of projects to skip (0–1000000).
37+
limit: Maximum number of projects to return (1–10000).
38+
39+
Returns:
40+
ProjectsPage: The requested window plus the total count.
41+
"""
42+
response = self._get(
43+
self._endpoint(_PROJECTS), params={"offset": offset, "limit": limit}
44+
)
45+
return ProjectsPage.model_validate(response.json())
46+
47+
@traced(name="projects_list", run_type="uipath")
48+
async def list_async(
49+
self, *, offset: int = 0, limit: int = DEFAULT_LIST_LIMIT
50+
) -> ProjectsPage:
51+
"""Asynchronously list projects visible to the caller.
52+
53+
Args:
54+
offset: Number of projects to skip (0–1000000).
55+
limit: Maximum number of projects to return (1–10000).
56+
57+
Returns:
58+
ProjectsPage: The requested window plus the total count.
59+
"""
60+
response = await self._get_async(
61+
self._endpoint(_PROJECTS), params={"offset": offset, "limit": limit}
62+
)
63+
return ProjectsPage.model_validate(response.json())
64+
65+
@traced(name="projects_retrieve", run_type="uipath")
66+
def retrieve(self, project_name: str) -> Project:
67+
"""Retrieve a project by its (slug) name.
68+
69+
Args:
70+
project_name: The backend project name (slug), as returned by
71+
:meth:`create` or :meth:`list`.
72+
73+
Returns:
74+
Project: The project.
75+
"""
76+
response = self._get(self._endpoint(_PROJECT, name=project_name))
77+
return Project.model_validate(response.json())
78+
79+
@traced(name="projects_retrieve", run_type="uipath")
80+
async def retrieve_async(self, project_name: str) -> Project:
81+
"""Asynchronously retrieve a project by its (slug) name.
82+
83+
Args:
84+
project_name: The backend project name (slug), as returned by
85+
:meth:`create_async` or :meth:`list_async`.
86+
87+
Returns:
88+
Project: The project.
89+
"""
90+
response = await self._get_async(self._endpoint(_PROJECT, name=project_name))
91+
return Project.model_validate(response.json())
92+
93+
@traced(name="projects_create", run_type="uipath")
94+
def create(self, name: str) -> Project:
95+
"""Create a project.
96+
97+
The API slugifies ``name`` server-side; the returned :attr:`Project.name`
98+
is the canonical slug used to address the project in every other call.
99+
100+
Args:
101+
name: Human-readable project name (1–116 characters).
102+
103+
Returns:
104+
Project: The created project.
105+
"""
106+
response = self._post(self._endpoint(_PROJECTS), body={"Name": name})
107+
return Project.model_validate(response.json())
108+
109+
@traced(name="projects_create", run_type="uipath")
110+
async def create_async(self, name: str) -> Project:
111+
"""Asynchronously create a project.
112+
113+
The API slugifies ``name`` server-side; the returned :attr:`Project.name`
114+
is the canonical slug used to address the project in every other call.
115+
116+
Args:
117+
name: Human-readable project name (1–116 characters).
118+
119+
Returns:
120+
Project: The created project.
121+
"""
122+
response = await self._post_async(
123+
self._endpoint(_PROJECTS), body={"Name": name}
124+
)
125+
return Project.model_validate(response.json())
126+
127+
@traced(name="projects_update", run_type="uipath")
128+
def update(self, project_name: str, *, title: str) -> Project:
129+
"""Update a project's display title.
130+
131+
``title`` is currently the only mutable field (keyword-only so the
132+
signature can grow without breaking callers).
133+
134+
Args:
135+
project_name: The backend project name (slug).
136+
title: New display title (1–1024 characters).
137+
138+
Returns:
139+
Project: The updated project.
140+
"""
141+
response = self._patch(
142+
self._endpoint(_PROJECT, name=project_name), body={"Title": title}
143+
)
144+
return Project.model_validate(response.json())
145+
146+
@traced(name="projects_update", run_type="uipath")
147+
async def update_async(self, project_name: str, *, title: str) -> Project:
148+
"""Asynchronously update a project's display title.
149+
150+
``title`` is currently the only mutable field (keyword-only so the
151+
signature can grow without breaking callers).
152+
153+
Args:
154+
project_name: The backend project name (slug).
155+
title: New display title (1–1024 characters).
156+
157+
Returns:
158+
Project: The updated project.
159+
"""
160+
response = await self._patch_async(
161+
self._endpoint(_PROJECT, name=project_name), body={"Title": title}
162+
)
163+
return Project.model_validate(response.json())
164+
165+
@traced(name="projects_delete", run_type="uipath")
166+
def delete(self, project_name: str) -> DeleteProjectResponse:
167+
"""Permanently delete a project and all its documents, taxonomy, and models.
168+
169+
Irreversible.
170+
171+
Args:
172+
project_name: The backend project name (slug).
173+
174+
Returns:
175+
DeleteProjectResponse: A success confirmation (``status == "ok"``).
176+
"""
177+
response = self._delete(self._endpoint(_PROJECT, name=project_name))
178+
return DeleteProjectResponse.model_validate(response.json())
179+
180+
@traced(name="projects_delete", run_type="uipath")
181+
async def delete_async(self, project_name: str) -> DeleteProjectResponse:
182+
"""Asynchronously and permanently delete a project and all its contents.
183+
184+
Irreversible.
185+
186+
Args:
187+
project_name: The backend project name (slug).
188+
189+
Returns:
190+
DeleteProjectResponse: A success confirmation (``status == "ok"``).
191+
"""
192+
response = await self._delete_async(self._endpoint(_PROJECT, name=project_name))
193+
return DeleteProjectResponse.model_validate(response.json())

0 commit comments

Comments
 (0)