codemie-sdk-python 0.1.417__tar.gz → 0.1.419__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/PKG-INFO +1 -1
  2. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/pyproject.toml +1 -1
  3. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/client/client.py +4 -0
  4. codemie_sdk_python-0.1.419/src/codemie_sdk/models/project.py +233 -0
  5. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/user.py +1 -0
  6. codemie_sdk_python-0.1.419/src/codemie_sdk/services/project.py +390 -0
  7. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/utils/http.py +39 -1
  8. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/README.md +0 -0
  9. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/__init__.py +0 -0
  10. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/auth/__init__.py +0 -0
  11. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/auth/credentials.py +0 -0
  12. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/client/__init__.py +0 -0
  13. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/exceptions.py +0 -0
  14. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/__init__.py +0 -0
  15. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/admin.py +0 -0
  16. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/analytics.py +0 -0
  17. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/assistant.py +0 -0
  18. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/categories.py +0 -0
  19. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/common.py +0 -0
  20. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/conversation.py +0 -0
  21. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/datasource.py +0 -0
  22. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/errors.py +0 -0
  23. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/file_operation.py +0 -0
  24. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/guardrails.py +0 -0
  25. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/integration.py +0 -0
  26. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/llm.py +0 -0
  27. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/mermaid.py +0 -0
  28. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/skill.py +0 -0
  29. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/task.py +0 -0
  30. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/vendor_assistant.py +0 -0
  31. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/vendor_guardrail.py +0 -0
  32. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/vendor_knowledgebase.py +0 -0
  33. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/vendor_workflow.py +0 -0
  34. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/workflow.py +0 -0
  35. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/workflow_execution_payload.py +0 -0
  36. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/workflow_state.py +0 -0
  37. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/models/workflow_thoughts.py +0 -0
  38. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/admin.py +0 -0
  39. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/analytics.py +0 -0
  40. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/assistant.py +0 -0
  41. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/categories.py +0 -0
  42. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/codemie_guardrails.py +0 -0
  43. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/conversation.py +0 -0
  44. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/datasource.py +0 -0
  45. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/files.py +0 -0
  46. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/integration.py +0 -0
  47. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/llm.py +0 -0
  48. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/mermaid.py +0 -0
  49. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/skill.py +0 -0
  50. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/task.py +0 -0
  51. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/user.py +0 -0
  52. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/vendor_assistant.py +0 -0
  53. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/vendor_guardrail.py +0 -0
  54. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/vendor_knowledgebase.py +0 -0
  55. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/vendor_workflow.py +0 -0
  56. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/webhook.py +0 -0
  57. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/workflow.py +0 -0
  58. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/workflow_execution.py +0 -0
  59. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/services/workflow_execution_state.py +0 -0
  60. {codemie_sdk_python-0.1.417 → codemie_sdk_python-0.1.419}/src/codemie_sdk/utils/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codemie-sdk-python
3
- Version: 0.1.417
3
+ Version: 0.1.419
4
4
  Summary: CodeMie SDK for Python
5
5
  Author: Vadym Vlasenko
6
6
  Author-email: vadym_vlasenko@epam.com
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "codemie-sdk-python"
3
- version = "0.1.417"
3
+ version = "0.1.419"
4
4
  description = "CodeMie SDK for Python"
5
5
  authors = [
6
6
  "Vadym Vlasenko <vadym_vlasenko@epam.com>",
@@ -12,6 +12,7 @@ from ..services.datasource import DatasourceService
12
12
  from ..services.llm import LLMService
13
13
  from ..services.integration import IntegrationService
14
14
  from ..services.mermaid import MermaidService
15
+ from ..services.project import ProjectService
15
16
  from ..services.skill import SkillService
16
17
  from ..services.task import TaskService
17
18
  from ..services.user import UserService
@@ -98,6 +99,9 @@ class CodeMieClient:
98
99
  self.integrations = IntegrationService(
99
100
  self._api_domain, self._token, verify_ssl=verify_ssl
100
101
  )
102
+ self.projects = ProjectService(
103
+ self._api_domain, self._token, verify_ssl=verify_ssl
104
+ )
101
105
  self.skills = SkillService(self._api_domain, self._token, verify_ssl=verify_ssl)
102
106
  self.tasks = TaskService(self._api_domain, self._token, verify_ssl=verify_ssl)
103
107
  self.users = UserService(self._api_domain, self._token, verify_ssl=verify_ssl)
@@ -0,0 +1,233 @@
1
+ # Copyright 2026 EPAM Systems, Inc. ("EPAM")
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Project management models for the CodeMie SDK."""
16
+
17
+ from datetime import datetime
18
+ from typing import Literal, Optional
19
+ from uuid import UUID
20
+
21
+ from pydantic import BaseModel, ConfigDict, Field
22
+
23
+
24
+ class ProjectCounters(BaseModel):
25
+ """Resource counters for a project."""
26
+
27
+ model_config = ConfigDict(extra="ignore")
28
+
29
+ assistants_count: int = 0
30
+ workflows_count: int = 0
31
+ integrations_count: int = 0
32
+ datasources_count: int = 0
33
+ skills_count: int = 0
34
+
35
+
36
+ class ProjectMember(BaseModel):
37
+ """Project member with role."""
38
+
39
+ model_config = ConfigDict(extra="ignore")
40
+
41
+ user_id: str
42
+ is_project_admin: bool
43
+ date: Optional[datetime] = None
44
+
45
+
46
+ class ProjectListItem(BaseModel):
47
+ """Project list response item."""
48
+
49
+ model_config = ConfigDict(extra="ignore")
50
+
51
+ name: str
52
+ description: Optional[str] = None
53
+ project_type: str
54
+ created_by: Optional[str] = None
55
+ user_count: int
56
+ admin_count: int
57
+ created_at: Optional[datetime] = None
58
+ counters: Optional[ProjectCounters] = None
59
+ cost_center_id: Optional[UUID] = None
60
+ cost_center_name: Optional[str] = None
61
+
62
+
63
+ class PaginationInfo(BaseModel):
64
+ """Pagination metadata for list responses."""
65
+
66
+ model_config = ConfigDict(extra="ignore")
67
+
68
+ total: int
69
+ page: int
70
+ per_page: int
71
+
72
+
73
+ class PaginatedProjectListResponse(BaseModel):
74
+ """Paginated project list response."""
75
+
76
+ model_config = ConfigDict(extra="ignore")
77
+
78
+ data: list[ProjectListItem]
79
+ pagination: PaginationInfo
80
+
81
+
82
+ class ProjectDetailResponse(BaseModel):
83
+ """Project detail response with member list."""
84
+
85
+ model_config = ConfigDict(extra="ignore")
86
+
87
+ name: str
88
+ description: Optional[str] = None
89
+ project_type: str
90
+ created_by: Optional[str] = None
91
+ user_count: int
92
+ admin_count: int
93
+ created_at: Optional[datetime] = None
94
+ cost_center_id: Optional[UUID] = None
95
+ cost_center_name: Optional[str] = None
96
+ members: list[ProjectMember]
97
+
98
+
99
+ class ProjectCreateRequest(BaseModel):
100
+ """Request to create a new project."""
101
+
102
+ model_config = ConfigDict(extra="ignore")
103
+
104
+ name: str
105
+ description: str = Field(description="Project description")
106
+ cost_center_id: Optional[UUID] = None
107
+
108
+
109
+ class ProjectCreateResponse(BaseModel):
110
+ """Response after creating a project."""
111
+
112
+ model_config = ConfigDict(extra="ignore")
113
+
114
+ name: str
115
+ description: str
116
+ project_type: str
117
+ created_by: str
118
+ created_at: datetime
119
+ cost_center_id: Optional[UUID] = None
120
+ cost_center_name: Optional[str] = None
121
+
122
+
123
+ # PATCH /v1/projects/{name} returns the same shape as the create response
124
+ ProjectUpdateResponse = ProjectCreateResponse
125
+
126
+
127
+ class ProjectUpdateRequest(BaseModel):
128
+ """Request to update a project."""
129
+
130
+ model_config = ConfigDict(extra="ignore")
131
+
132
+ name: Optional[str] = None
133
+ description: Optional[str] = None
134
+ cost_center_id: Optional[UUID] = None
135
+ clear_cost_center: bool = False
136
+
137
+
138
+ class ProjectDeleteResponse(BaseModel):
139
+ """Response after deleting a project."""
140
+
141
+ model_config = ConfigDict(extra="ignore")
142
+
143
+ message: str
144
+ name: str
145
+
146
+
147
+ class ProjectAssignmentRequest(BaseModel):
148
+ """Request to assign a user to a project."""
149
+
150
+ model_config = ConfigDict(extra="ignore")
151
+
152
+ user_id: str
153
+ is_project_admin: bool
154
+
155
+
156
+ class ProjectAssignmentUpdateRequest(BaseModel):
157
+ """Request to update a user's project role."""
158
+
159
+ model_config = ConfigDict(extra="ignore")
160
+
161
+ is_project_admin: bool
162
+
163
+
164
+ class ProjectAssignmentResponse(BaseModel):
165
+ """Response after project assignment operation."""
166
+
167
+ model_config = ConfigDict(extra="ignore")
168
+
169
+ message: str
170
+ user_id: str
171
+ project_name: str
172
+ is_project_admin: Optional[bool] = None
173
+
174
+
175
+ class BulkAssignmentUserItem(BaseModel):
176
+ """Single user entry in bulk assignment request."""
177
+
178
+ model_config = ConfigDict(extra="ignore")
179
+
180
+ user_id: str
181
+ is_project_admin: bool
182
+
183
+
184
+ class BulkAssignmentRequest(BaseModel):
185
+ """Bulk assignment request body."""
186
+
187
+ model_config = ConfigDict(extra="ignore")
188
+
189
+ users: list[BulkAssignmentUserItem] = Field(
190
+ ...,
191
+ min_length=1,
192
+ max_length=1000,
193
+ description="List of users to assign/update (1-1000)",
194
+ )
195
+
196
+
197
+ class BulkAssignmentResultItem(BaseModel):
198
+ """Per-user result in bulk assignment response."""
199
+
200
+ model_config = ConfigDict(extra="ignore")
201
+
202
+ user_id: str
203
+ action: Literal["assigned", "updated", "removed"]
204
+ is_project_admin: Optional[bool] = None
205
+
206
+
207
+ class BulkAssignmentResponse(BaseModel):
208
+ """Response for bulk assignment operations."""
209
+
210
+ model_config = ConfigDict(extra="ignore")
211
+
212
+ message: str
213
+ project_name: str
214
+ total: int
215
+ results: list[BulkAssignmentResultItem]
216
+
217
+
218
+ class CsvImportRowResult(BaseModel):
219
+ """Per-row result from a CSV import validation."""
220
+
221
+ model_config = ConfigDict(extra="ignore")
222
+
223
+ email: str
224
+ role: str
225
+ error: Optional[str] = None
226
+
227
+
228
+ class CsvImportValidationResponse(BaseModel):
229
+ """Response for CSV import dry-run validation."""
230
+
231
+ model_config = ConfigDict(extra="ignore")
232
+
233
+ users: list[CsvImportRowResult]
@@ -14,6 +14,7 @@ class User(BaseModel):
14
14
  )
15
15
  name: str = Field(description="Full name of the user")
16
16
  username: str = Field(description="Username for authentication")
17
+ email: str = Field(description="User email")
17
18
  is_admin: bool = Field(
18
19
  description="Whether the user has admin privileges", validation_alias="isAdmin"
19
20
  )
@@ -0,0 +1,390 @@
1
+ # Copyright 2026 EPAM Systems, Inc. ("EPAM")
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Project management service for the CodeMie SDK."""
16
+
17
+ from typing import Literal, Optional
18
+ from uuid import UUID
19
+
20
+ from ..models.project import (
21
+ BulkAssignmentRequest,
22
+ BulkAssignmentResponse,
23
+ BulkAssignmentUserItem,
24
+ CsvImportValidationResponse,
25
+ PaginatedProjectListResponse,
26
+ ProjectAssignmentRequest,
27
+ ProjectAssignmentResponse,
28
+ ProjectAssignmentUpdateRequest,
29
+ ProjectCreateRequest,
30
+ ProjectCreateResponse,
31
+ ProjectDeleteResponse,
32
+ ProjectDetailResponse,
33
+ ProjectUpdateRequest,
34
+ ProjectUpdateResponse,
35
+ )
36
+ from ..utils.http import ApiRequestHandler
37
+
38
+
39
+ class ProjectService:
40
+ """Service for managing projects in CodeMie."""
41
+
42
+ def __init__(self, api_domain: str, token: str, verify_ssl: bool = True):
43
+ """Initialize the ProjectService.
44
+
45
+ Args:
46
+ api_domain: Base URL for the CodeMie API
47
+ token: Authentication token
48
+ verify_ssl: Whether to verify SSL certificates
49
+ """
50
+ self._api = ApiRequestHandler(api_domain, token, verify_ssl)
51
+
52
+ def create_project(
53
+ self,
54
+ name: str,
55
+ description: str,
56
+ cost_center_id: Optional[UUID] = None,
57
+ ) -> ProjectCreateResponse:
58
+ """Create a new shared project.
59
+
60
+ Args:
61
+ name: Project name
62
+ description: Project description
63
+ cost_center_id: Optional cost center ID
64
+
65
+ Returns:
66
+ ProjectCreateResponse with project details
67
+
68
+ Raises:
69
+ HTTPException: If project creation fails
70
+ """
71
+ payload = ProjectCreateRequest(
72
+ name=name,
73
+ description=description,
74
+ cost_center_id=cost_center_id,
75
+ )
76
+
77
+ return self._api.post(
78
+ "/v1/projects",
79
+ response_model=ProjectCreateResponse,
80
+ json_data=payload.model_dump(exclude_none=True),
81
+ )
82
+
83
+ def list_projects(
84
+ self,
85
+ search: Optional[str] = None,
86
+ page: int = 0,
87
+ per_page: int = 20,
88
+ include_counters: bool = True,
89
+ sort_by: Optional[Literal["name", "created_at"]] = None,
90
+ sort_order: Literal["asc", "desc"] = "asc",
91
+ ) -> PaginatedProjectListResponse:
92
+ """List projects visible to current user with pagination and search.
93
+
94
+ Args:
95
+ search: Search by project name or description (substring match)
96
+ page: Page number (0-indexed)
97
+ per_page: Items per page (10-100)
98
+ sort_by: Sort field (name or created_at); ignored when search is active
99
+ sort_order: Sort direction (asc or desc)
100
+
101
+ Returns:
102
+ PaginatedProjectListResponse with projects and pagination info
103
+ """
104
+ params = {
105
+ "page": page,
106
+ "per_page": per_page,
107
+ "include_counters": include_counters,
108
+ "sort_order": sort_order,
109
+ }
110
+
111
+ if search is not None:
112
+ params["search"] = search
113
+ if sort_by is not None:
114
+ params["sort_by"] = sort_by
115
+
116
+ return self._api.get(
117
+ "/v1/projects",
118
+ response_model=PaginatedProjectListResponse,
119
+ params=params,
120
+ wrap_response=False,
121
+ )
122
+
123
+ def get_project_detail(
124
+ self,
125
+ project_name: str,
126
+ ) -> ProjectDetailResponse:
127
+ """Get project detail with member list.
128
+
129
+ Args:
130
+ project_name: Project name
131
+
132
+ Returns:
133
+ ProjectDetailResponse with project details and member list
134
+
135
+ Raises:
136
+ HTTPException: If project not found or user doesn't have access (404)
137
+ """
138
+ return self._api.get(
139
+ f"/v1/projects/{project_name}",
140
+ response_model=ProjectDetailResponse,
141
+ )
142
+
143
+ def update_project(
144
+ self,
145
+ project_name: str,
146
+ name: Optional[str] = None,
147
+ description: Optional[str] = None,
148
+ cost_center_id: Optional[UUID] = None,
149
+ clear_cost_center: bool = False,
150
+ ) -> ProjectUpdateResponse:
151
+ """Update a project.
152
+
153
+ Args:
154
+ project_name: Current project name
155
+ name: New project name (optional)
156
+ description: New description (optional)
157
+ cost_center_id: New cost center ID (optional)
158
+ clear_cost_center: Set to True to remove cost center
159
+
160
+ Returns:
161
+ ProjectUpdateResponse with updated project details
162
+
163
+ Raises:
164
+ HTTPException: If project update fails
165
+ """
166
+ payload = ProjectUpdateRequest(
167
+ name=name,
168
+ description=description,
169
+ cost_center_id=cost_center_id,
170
+ clear_cost_center=clear_cost_center,
171
+ )
172
+
173
+ return self._api.patch(
174
+ f"/v1/projects/{project_name}",
175
+ response_model=ProjectUpdateResponse,
176
+ json_data=payload.model_dump(exclude_none=True),
177
+ )
178
+
179
+ def delete_project(self, project_name: str) -> ProjectDeleteResponse:
180
+ """Hard-delete a project if it has no assigned resources.
181
+
182
+ Args:
183
+ project_name: Project name
184
+
185
+ Returns:
186
+ ProjectDeleteResponse with deletion confirmation
187
+
188
+ Raises:
189
+ HTTPException: If project has assigned resources (409) or is personal (403)
190
+ """
191
+ return self._api.delete(
192
+ f"/v1/projects/{project_name}",
193
+ response_model=ProjectDeleteResponse,
194
+ )
195
+
196
+ # Assignment endpoints
197
+
198
+ def assign_user_to_project(
199
+ self,
200
+ project_name: str,
201
+ user_id: str,
202
+ is_project_admin: bool,
203
+ ) -> ProjectAssignmentResponse:
204
+ """Assign a user to a project.
205
+
206
+ Args:
207
+ project_name: Project name
208
+ user_id: User ID to assign
209
+ is_project_admin: Whether user should be project admin
210
+
211
+ Returns:
212
+ ProjectAssignmentResponse with assignment details
213
+
214
+ Raises:
215
+ HTTPException: If assignment fails
216
+ """
217
+ payload = ProjectAssignmentRequest(
218
+ user_id=user_id,
219
+ is_project_admin=is_project_admin,
220
+ )
221
+
222
+ return self._api.post(
223
+ f"/v1/projects/{project_name}/assignment",
224
+ response_model=ProjectAssignmentResponse,
225
+ json_data=payload.model_dump(exclude_none=True),
226
+ )
227
+
228
+ def update_user_project_assignment(
229
+ self,
230
+ project_name: str,
231
+ user_id: str,
232
+ is_project_admin: bool,
233
+ ) -> ProjectAssignmentResponse:
234
+ """Update a user's project-admin flag.
235
+
236
+ Args:
237
+ project_name: Project name
238
+ user_id: User ID to update
239
+ is_project_admin: New project admin flag value
240
+
241
+ Returns:
242
+ ProjectAssignmentResponse with updated assignment
243
+
244
+ Raises:
245
+ HTTPException: If user is not assigned to project (404)
246
+ """
247
+ payload = ProjectAssignmentUpdateRequest(is_project_admin=is_project_admin)
248
+
249
+ return self._api.put(
250
+ f"/v1/projects/{project_name}/assignment/{user_id}",
251
+ response_model=ProjectAssignmentResponse,
252
+ json_data=payload.model_dump(exclude_none=True),
253
+ )
254
+
255
+ def remove_user_from_project(
256
+ self,
257
+ project_name: str,
258
+ user_id: str,
259
+ ) -> ProjectAssignmentResponse:
260
+ """Remove a user from a project.
261
+
262
+ Args:
263
+ project_name: Project name
264
+ user_id: User ID to remove
265
+
266
+ Returns:
267
+ ProjectAssignmentResponse with removal confirmation
268
+
269
+ Raises:
270
+ HTTPException: If user is not assigned to project (404)
271
+ """
272
+ return self._api.delete(
273
+ f"/v1/projects/{project_name}/assignment/{user_id}",
274
+ response_model=ProjectAssignmentResponse,
275
+ )
276
+
277
+ def bulk_assign_users_to_project(
278
+ self,
279
+ project_name: str,
280
+ users: list[BulkAssignmentUserItem],
281
+ ) -> BulkAssignmentResponse:
282
+ """Bulk assign/upsert users to a project.
283
+
284
+ Assigns new users and updates roles for existing members in a single
285
+ atomic operation.
286
+
287
+ Args:
288
+ project_name: Project name
289
+ users: List of BulkAssignmentUserItem with user_id and is_project_admin (1-1000)
290
+
291
+ Returns:
292
+ BulkAssignmentResponse with per-user results
293
+
294
+ Raises:
295
+ HTTPException: If bulk assignment fails
296
+ """
297
+ payload = BulkAssignmentRequest(users=users)
298
+
299
+ return self._api.post(
300
+ f"/v1/projects/{project_name}/assignments",
301
+ response_model=BulkAssignmentResponse,
302
+ json_data=payload.model_dump(),
303
+ )
304
+
305
+ def bulk_remove_users_from_project(
306
+ self,
307
+ project_name: str,
308
+ user_ids: list[str],
309
+ ) -> BulkAssignmentResponse:
310
+ """Bulk remove users from a project.
311
+
312
+ Args:
313
+ project_name: Project name
314
+ user_ids: List of user IDs to remove (1-100)
315
+
316
+ Returns:
317
+ BulkAssignmentResponse with per-user results
318
+
319
+ Raises:
320
+ HTTPException: If bulk removal fails
321
+ """
322
+ params = [("user_id", user_id) for user_id in user_ids]
323
+
324
+ return self._api.delete(
325
+ f"/v1/projects/{project_name}/assignments",
326
+ response_model=BulkAssignmentResponse,
327
+ params=params,
328
+ )
329
+
330
+ # CSV import endpoints
331
+
332
+ def validate_users_from_csv(
333
+ self,
334
+ project_name: str,
335
+ file_content: bytes,
336
+ filename: str = "users.csv",
337
+ ) -> CsvImportValidationResponse:
338
+ """Dry-run validation of a CSV file for user import.
339
+
340
+ Validates each row (email format, role, system user existence) without
341
+ modifying any data.
342
+
343
+ Args:
344
+ project_name: Project name
345
+ file_content: CSV file content as bytes
346
+ filename: CSV filename
347
+
348
+ Returns:
349
+ CsvImportValidationResponse with per-row validation results
350
+
351
+ Raises:
352
+ HTTPException: If file too large (413) or structural CSV error (422)
353
+ """
354
+ files = {"file": (filename, file_content, "text/csv")}
355
+
356
+ return self._api.post_multipart(
357
+ f"/v1/projects/{project_name}/import-users/validate",
358
+ response_model=CsvImportValidationResponse,
359
+ files=files,
360
+ )
361
+
362
+ def import_users_from_csv(
363
+ self,
364
+ project_name: str,
365
+ file_content: bytes,
366
+ filename: str = "users.csv",
367
+ ) -> BulkAssignmentResponse:
368
+ """Assign users to a project from a CSV file upload.
369
+
370
+ CSV must include a header row with 'email' and 'role' columns.
371
+ Allowed roles: 'administrator' (project admin), 'user' (regular member).
372
+
373
+ Args:
374
+ project_name: Project name
375
+ file_content: CSV file content as bytes
376
+ filename: CSV filename
377
+
378
+ Returns:
379
+ BulkAssignmentResponse with per-user results
380
+
381
+ Raises:
382
+ HTTPException: If file too large (413) or validation fails (422)
383
+ """
384
+ files = {"file": (filename, file_content, "text/csv")}
385
+
386
+ return self._api.post_multipart(
387
+ f"/v1/projects/{project_name}/import-users",
388
+ response_model=BulkAssignmentResponse,
389
+ files=files,
390
+ )
@@ -292,15 +292,52 @@ class ApiRequestHandler:
292
292
  response.raise_for_status()
293
293
  return self._parse_response(response, response_model, wrap_response)
294
294
 
295
+ @log_request
296
+ def patch(
297
+ self,
298
+ endpoint: str,
299
+ response_model: Type[T],
300
+ json_data: Dict[str, Any],
301
+ params: Optional[Dict[str, Any]] = None,
302
+ wrap_response: bool = True,
303
+ ) -> T:
304
+ """Makes a PATCH request and parses the response.
305
+
306
+ Args:
307
+ endpoint: API endpoint path
308
+ response_model: Pydantic model class or List[Model] for response
309
+ json_data: JSON request body
310
+ params: Optional query parameters (e.g., filters, pagination)
311
+ wrap_response: Whether response is wrapped in 'data' field
312
+
313
+ Returns:
314
+ Parsed response object or list of objects
315
+ """
316
+ logger.debug(f"Request body: {json_data}")
317
+ response = requests.patch(
318
+ url=f"{self._base_url}{endpoint}",
319
+ headers=self._get_headers(),
320
+ json=json_data,
321
+ params=params,
322
+ verify=self._verify_ssl,
323
+ )
324
+ response.raise_for_status()
325
+ return self._parse_response(response, response_model, wrap_response)
326
+
295
327
  @log_request
296
328
  def delete(
297
- self, endpoint: str, response_model: Type[T], wrap_response: bool = True
329
+ self,
330
+ endpoint: str,
331
+ response_model: Type[T],
332
+ params: Optional[Dict[str, Any]] = None,
333
+ wrap_response: bool = True,
298
334
  ) -> T:
299
335
  """Makes a DELETE request and parses the response.
300
336
 
301
337
  Args:
302
338
  endpoint: API endpoint path
303
339
  response_model: Pydantic model class or List[Model] for response
340
+ params: Query parameters (for passing multiple user_id params for bulk delete)
304
341
  wrap_response: Whether response is wrapped in 'data' field
305
342
 
306
343
  Returns:
@@ -309,6 +346,7 @@ class ApiRequestHandler:
309
346
  response = requests.delete(
310
347
  url=f"{self._base_url}{endpoint}",
311
348
  headers=self._get_headers(),
349
+ params=params,
312
350
  verify=self._verify_ssl,
313
351
  )
314
352
  response.raise_for_status()