tallyfy 1.0.3__py3-none-any.whl → 1.0.5__py3-none-any.whl
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.
Potentially problematic release.
This version of tallyfy might be problematic. Click here for more details.
- tallyfy/__init__.py +8 -4
- tallyfy/core.py +8 -8
- tallyfy/form_fields_management/__init__.py +70 -0
- tallyfy/form_fields_management/base.py +109 -0
- tallyfy/form_fields_management/crud_operations.py +234 -0
- tallyfy/form_fields_management/options_management.py +222 -0
- tallyfy/form_fields_management/suggestions.py +411 -0
- tallyfy/task_management/__init__.py +81 -0
- tallyfy/task_management/base.py +125 -0
- tallyfy/task_management/creation.py +221 -0
- tallyfy/task_management/retrieval.py +211 -0
- tallyfy/task_management/search.py +196 -0
- tallyfy/template_management/__init__.py +85 -0
- tallyfy/template_management/analysis.py +1093 -0
- tallyfy/template_management/automation.py +469 -0
- tallyfy/template_management/base.py +56 -0
- tallyfy/template_management/basic_operations.py +477 -0
- tallyfy/template_management/health_assessment.py +763 -0
- tallyfy/user_management/__init__.py +69 -0
- tallyfy/user_management/base.py +146 -0
- tallyfy/user_management/invitation.py +286 -0
- tallyfy/user_management/retrieval.py +339 -0
- {tallyfy-1.0.3.dist-info → tallyfy-1.0.5.dist-info}/METADATA +120 -56
- tallyfy-1.0.5.dist-info/RECORD +28 -0
- tallyfy/BUILD.md +0 -5
- tallyfy/README.md +0 -634
- tallyfy/form_fields_management.py +0 -582
- tallyfy/task_management.py +0 -356
- tallyfy/template_management.py +0 -2607
- tallyfy/user_management.py +0 -235
- tallyfy-1.0.3.dist-info/RECORD +0 -14
- {tallyfy-1.0.3.dist-info → tallyfy-1.0.5.dist-info}/WHEEL +0 -0
- {tallyfy-1.0.3.dist-info → tallyfy-1.0.5.dist-info}/licenses/LICENSE +0 -0
- {tallyfy-1.0.3.dist-info → tallyfy-1.0.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic CRUD operations for template management
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional, Dict, Any
|
|
6
|
+
from .base import TemplateManagerBase
|
|
7
|
+
from ..models import Template, Step, TallyfyError, TemplatesList
|
|
8
|
+
from email_validator import validate_email, EmailNotValidError
|
|
9
|
+
|
|
10
|
+
class TemplateBasicOperations(TemplateManagerBase):
|
|
11
|
+
"""Handles basic template CRUD operations"""
|
|
12
|
+
|
|
13
|
+
def search_templates_by_name(self, org_id: str, template_name: str) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Search for template by name using the search endpoint.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
org_id: Organization ID
|
|
19
|
+
template_name: Name or partial name of the template to search for
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Template ID of the found template
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
TallyfyError: If no template found, multiple matches, or search fails
|
|
26
|
+
"""
|
|
27
|
+
self._validate_org_id(org_id)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
search_endpoint = f"organizations/{org_id}/search"
|
|
31
|
+
search_params = {
|
|
32
|
+
'on': 'blueprint',
|
|
33
|
+
'per_page': '20',
|
|
34
|
+
'search': template_name
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
search_response = self.sdk._make_request('GET', search_endpoint, params=search_params)
|
|
38
|
+
|
|
39
|
+
if isinstance(search_response, dict) and 'blueprint' in search_response:
|
|
40
|
+
template_data = search_response['blueprint']
|
|
41
|
+
if 'data' in template_data and template_data['data']:
|
|
42
|
+
templates = template_data['data']
|
|
43
|
+
|
|
44
|
+
# First try exact match (case-insensitive)
|
|
45
|
+
exact_matches = [p for p in templates if p['title'].lower() == template_name.lower()]
|
|
46
|
+
if exact_matches:
|
|
47
|
+
return exact_matches[0]['id']
|
|
48
|
+
elif len(templates) == 1:
|
|
49
|
+
# Single search result, use it
|
|
50
|
+
return templates[0]['id']
|
|
51
|
+
else:
|
|
52
|
+
# Multiple matches found, provide helpful error with options
|
|
53
|
+
match_names = [f"'{p['title']}'" for p in templates[:10]] # Show max 10
|
|
54
|
+
raise TallyfyError(
|
|
55
|
+
f"Multiple templates found matching '{template_name}': {', '.join(match_names)}. Please be more specific.")
|
|
56
|
+
else:
|
|
57
|
+
raise TallyfyError(f"No template found matching name: {template_name}")
|
|
58
|
+
else:
|
|
59
|
+
raise TallyfyError(f"Search failed for template name: {template_name}")
|
|
60
|
+
|
|
61
|
+
except TallyfyError:
|
|
62
|
+
raise
|
|
63
|
+
except Exception as e:
|
|
64
|
+
self._handle_api_error(e, "search templates by name", org_id=org_id, template_name=template_name)
|
|
65
|
+
|
|
66
|
+
def get_template(self, org_id: str, template_id: Optional[str] = None, template_name: Optional[str] = None) -> Optional[Template]:
|
|
67
|
+
"""
|
|
68
|
+
Get a template (checklist) by its ID or name with full details including prerun fields,
|
|
69
|
+
automated actions, linked tasks, and metadata.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
org_id: Organization ID
|
|
73
|
+
template_id: Template (checklist) ID
|
|
74
|
+
template_name: Template (checklist) name
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Template object with complete template data
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
TallyfyError: If the request fails
|
|
81
|
+
"""
|
|
82
|
+
if not template_id and not template_name:
|
|
83
|
+
raise ValueError("Either template_id or template_name must be provided")
|
|
84
|
+
|
|
85
|
+
self._validate_org_id(org_id)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# If template_name is provided but not template_id, search for the template first
|
|
89
|
+
if template_name and not template_id:
|
|
90
|
+
template_id = self.search_templates_by_name(org_id, template_name)
|
|
91
|
+
|
|
92
|
+
endpoint = f"organizations/{org_id}/checklists/{template_id}"
|
|
93
|
+
response_data = self.sdk._make_request('GET', endpoint)
|
|
94
|
+
|
|
95
|
+
template_data = self._extract_data(response_data)
|
|
96
|
+
if template_data:
|
|
97
|
+
return Template.from_dict(template_data)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
except TallyfyError:
|
|
101
|
+
raise
|
|
102
|
+
except Exception as e:
|
|
103
|
+
self._handle_api_error(e, "get template", org_id=org_id, template_id=template_id)
|
|
104
|
+
|
|
105
|
+
def get_all_templates(self, org_id: str) -> TemplatesList:
|
|
106
|
+
"""
|
|
107
|
+
Get all templates (checklists) for an organization with pagination metadata.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
org_id: Organization ID
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
TemplatesList object containing list of templates and pagination metadata
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
TallyfyError: If the request fails
|
|
117
|
+
"""
|
|
118
|
+
self._validate_org_id(org_id)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
endpoint = f"organizations/{org_id}/checklists"
|
|
122
|
+
response_data = self.sdk._make_request('GET', endpoint)
|
|
123
|
+
|
|
124
|
+
if isinstance(response_data, dict):
|
|
125
|
+
return TemplatesList.from_dict(response_data)
|
|
126
|
+
else:
|
|
127
|
+
self.sdk.logger.warning("Unexpected response format for templates list")
|
|
128
|
+
return TemplatesList(data=[], meta=None)
|
|
129
|
+
|
|
130
|
+
except TallyfyError:
|
|
131
|
+
raise
|
|
132
|
+
except Exception as e:
|
|
133
|
+
self._handle_api_error(e, "get all templates", org_id=org_id)
|
|
134
|
+
|
|
135
|
+
def update_template_metadata(self, org_id: str, template_id: str, **kwargs) -> Optional[Template]:
|
|
136
|
+
"""
|
|
137
|
+
Update template metadata like title, summary, guidance, icons, etc.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
org_id: Organization ID
|
|
141
|
+
template_id: Template ID to update
|
|
142
|
+
**kwargs: Template metadata fields to update (title, summary, guidance, icon, etc.)
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Updated Template object
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
TallyfyError: If the request fails
|
|
149
|
+
"""
|
|
150
|
+
self._validate_org_id(org_id)
|
|
151
|
+
self._validate_template_id(template_id)
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
endpoint = f"organizations/{org_id}/checklists/{template_id}"
|
|
155
|
+
|
|
156
|
+
# Build update data from kwargs
|
|
157
|
+
update_data = {}
|
|
158
|
+
allowed_fields = [
|
|
159
|
+
'title', 'summary', 'guidance', 'icon', 'alias', 'webhook',
|
|
160
|
+
'explanation_video', 'kickoff_title', 'kickoff_description',
|
|
161
|
+
'is_public', 'is_featured', 'auto_naming', 'folderize_process',
|
|
162
|
+
'tag_process', 'allow_launcher_change_name', 'is_pinned',
|
|
163
|
+
'default_folder', 'folder_changeable_by_launcher'
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
for field, value in kwargs.items():
|
|
167
|
+
if field in allowed_fields:
|
|
168
|
+
update_data[field] = value
|
|
169
|
+
else:
|
|
170
|
+
self.sdk.logger.warning(f"Ignoring unknown template field: {field}")
|
|
171
|
+
|
|
172
|
+
if not update_data:
|
|
173
|
+
raise ValueError("No valid template fields provided for update")
|
|
174
|
+
|
|
175
|
+
response_data = self.sdk._make_request('PUT', endpoint, data=update_data)
|
|
176
|
+
|
|
177
|
+
template_data = self._extract_data(response_data)
|
|
178
|
+
if template_data:
|
|
179
|
+
return Template.from_dict(template_data)
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
except TallyfyError:
|
|
183
|
+
raise
|
|
184
|
+
except Exception as e:
|
|
185
|
+
self._handle_api_error(e, "update template metadata", org_id=org_id, template_id=template_id)
|
|
186
|
+
|
|
187
|
+
def get_template_with_steps(self, org_id: str, template_id: Optional[str] = None, template_name: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
188
|
+
"""
|
|
189
|
+
Get template with full step details and structure.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
org_id: Organization ID
|
|
193
|
+
template_id: Template ID to retrieve
|
|
194
|
+
template_name: Template name to retrieve (alternative to template_id)
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Dictionary containing template data with full step details
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
TallyfyError: If the request fails
|
|
201
|
+
"""
|
|
202
|
+
if not template_id and not template_name:
|
|
203
|
+
raise ValueError("Either template_id or template_name must be provided")
|
|
204
|
+
|
|
205
|
+
self._validate_org_id(org_id)
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
# If template_name is provided but not template_id, search for the template first
|
|
209
|
+
if template_name and not template_id:
|
|
210
|
+
template_id = self.search_templates_by_name(org_id, template_name)
|
|
211
|
+
|
|
212
|
+
# Get template with steps included
|
|
213
|
+
endpoint = f"organizations/{org_id}/checklists/{template_id}"
|
|
214
|
+
params = {'with': 'steps,automated_actions,prerun'}
|
|
215
|
+
|
|
216
|
+
response_data = self.sdk._make_request('GET', endpoint, params=params)
|
|
217
|
+
|
|
218
|
+
template_data = self._extract_data(response_data)
|
|
219
|
+
if template_data:
|
|
220
|
+
return {
|
|
221
|
+
'template': Template.from_dict(template_data),
|
|
222
|
+
'raw_data': template_data,
|
|
223
|
+
'step_count': len(template_data.get('steps', [])),
|
|
224
|
+
'steps': template_data.get('steps', []),
|
|
225
|
+
'automation_count': len(template_data.get('automated_actions', [])),
|
|
226
|
+
'prerun_field_count': len(template_data.get('prerun', []))
|
|
227
|
+
}
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
except TallyfyError:
|
|
231
|
+
raise
|
|
232
|
+
except Exception as e:
|
|
233
|
+
self._handle_api_error(e, "get template with steps", org_id=org_id, template_id=template_id)
|
|
234
|
+
|
|
235
|
+
def duplicate_template(self, org_id: str, template_id: str, new_name: str, copy_permissions: bool = False) -> Optional[Template]:
|
|
236
|
+
"""
|
|
237
|
+
Create a copy of a template for safe editing.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
org_id: Organization ID
|
|
241
|
+
template_id: Template ID to duplicate
|
|
242
|
+
new_name: Name for the new template copy
|
|
243
|
+
copy_permissions: Whether to copy template permissions (default: False)
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
New Template object
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
TallyfyError: If the request fails
|
|
250
|
+
"""
|
|
251
|
+
self._validate_org_id(org_id)
|
|
252
|
+
self._validate_template_id(template_id)
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
endpoint = f"organizations/{org_id}/checklists/{template_id}/duplicate"
|
|
256
|
+
|
|
257
|
+
duplicate_data = {
|
|
258
|
+
'title': new_name,
|
|
259
|
+
'copy_permissions': copy_permissions
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
response_data = self.sdk._make_request('POST', endpoint, data=duplicate_data)
|
|
263
|
+
|
|
264
|
+
template_data = self._extract_data(response_data)
|
|
265
|
+
if template_data:
|
|
266
|
+
return Template.from_dict(template_data)
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
except TallyfyError:
|
|
270
|
+
raise
|
|
271
|
+
except Exception as e:
|
|
272
|
+
self._handle_api_error(e, "duplicate template", org_id=org_id, template_id=template_id)
|
|
273
|
+
|
|
274
|
+
def get_template_steps(self, org_id: str, template_id: str) -> List[Step]:
|
|
275
|
+
"""
|
|
276
|
+
Get all steps of a template.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
org_id: Organization ID
|
|
280
|
+
template_id: Template ID
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
List of Step objects
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
TallyfyError: If the request fails
|
|
287
|
+
"""
|
|
288
|
+
self._validate_org_id(org_id)
|
|
289
|
+
self._validate_template_id(template_id)
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
endpoint = f"organizations/{org_id}/checklists/{template_id}/steps"
|
|
293
|
+
response_data = self.sdk._make_request('GET', endpoint)
|
|
294
|
+
|
|
295
|
+
if isinstance(response_data, dict) and 'data' in response_data:
|
|
296
|
+
steps_data = response_data['data']
|
|
297
|
+
return [Step.from_dict(step_data) for step_data in steps_data]
|
|
298
|
+
elif isinstance(response_data, list):
|
|
299
|
+
return [Step.from_dict(step_data) for step_data in response_data]
|
|
300
|
+
else:
|
|
301
|
+
self.sdk.logger.warning("Unexpected response format for template steps")
|
|
302
|
+
return []
|
|
303
|
+
|
|
304
|
+
except TallyfyError:
|
|
305
|
+
raise
|
|
306
|
+
except Exception as e:
|
|
307
|
+
self._handle_api_error(e, "get template steps", org_id=org_id, template_id=template_id)
|
|
308
|
+
|
|
309
|
+
def edit_description_on_step(self, org_id: str, template_id: str, step_id: str, description: str) -> Dict[str, Any]:
|
|
310
|
+
"""
|
|
311
|
+
Edit the description/summary of a specific step in a template.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
org_id: Organization ID
|
|
315
|
+
template_id: Template ID
|
|
316
|
+
step_id: Step ID to edit description for
|
|
317
|
+
description: New description/summary text for the step
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Dictionary containing updated step information
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
TallyfyError: If the request fails
|
|
324
|
+
"""
|
|
325
|
+
self._validate_org_id(org_id)
|
|
326
|
+
self._validate_template_id(template_id)
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
endpoint = f"organizations/{org_id}/checklists/{template_id}/steps/{step_id}"
|
|
330
|
+
|
|
331
|
+
# Validate description
|
|
332
|
+
if not isinstance(description, str):
|
|
333
|
+
raise ValueError("Description must be a string")
|
|
334
|
+
|
|
335
|
+
description = description.strip()
|
|
336
|
+
if not description:
|
|
337
|
+
raise ValueError("Description cannot be empty")
|
|
338
|
+
|
|
339
|
+
# Update data with correct payload structure
|
|
340
|
+
update_data = {
|
|
341
|
+
'summary': description
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
response_data = self.sdk._make_request('PUT', endpoint, data=update_data)
|
|
345
|
+
|
|
346
|
+
if isinstance(response_data, dict):
|
|
347
|
+
if 'data' in response_data:
|
|
348
|
+
return response_data['data']
|
|
349
|
+
return response_data
|
|
350
|
+
else:
|
|
351
|
+
self.sdk.logger.warning("Unexpected response format for step description update")
|
|
352
|
+
return {'success': True, 'updated_summary': description}
|
|
353
|
+
|
|
354
|
+
except TallyfyError:
|
|
355
|
+
raise
|
|
356
|
+
except ValueError as e:
|
|
357
|
+
raise TallyfyError(f"Invalid description data: {e}")
|
|
358
|
+
except Exception as e:
|
|
359
|
+
self._handle_api_error(e, "edit step description", org_id=org_id, template_id=template_id, step_id=step_id)
|
|
360
|
+
|
|
361
|
+
def add_step_to_template(self, org_id: str, template_id: str, step_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
362
|
+
"""
|
|
363
|
+
Add a new step to a template.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
org_id: Organization ID
|
|
367
|
+
template_id: Template ID
|
|
368
|
+
step_data: Dictionary containing step data including title, summary, position, etc.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Dictionary containing created step information
|
|
372
|
+
|
|
373
|
+
Raises:
|
|
374
|
+
TallyfyError: If the request fails
|
|
375
|
+
"""
|
|
376
|
+
self._validate_org_id(org_id)
|
|
377
|
+
self._validate_template_id(template_id)
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
endpoint = f"organizations/{org_id}/checklists/{template_id}/steps"
|
|
381
|
+
|
|
382
|
+
# Validate step data
|
|
383
|
+
if not isinstance(step_data, dict):
|
|
384
|
+
raise ValueError("Step data must be a dictionary")
|
|
385
|
+
|
|
386
|
+
# Validate required fields
|
|
387
|
+
if 'title' not in step_data or not step_data['title']:
|
|
388
|
+
raise ValueError("Step title is required")
|
|
389
|
+
|
|
390
|
+
title = step_data['title'].strip()
|
|
391
|
+
if not title:
|
|
392
|
+
raise ValueError("Step title cannot be empty")
|
|
393
|
+
|
|
394
|
+
# Build step creation data with defaults
|
|
395
|
+
create_data = {'title': title}
|
|
396
|
+
|
|
397
|
+
# Add optional string fields
|
|
398
|
+
optional_string_fields = ['summary', 'step_type', 'alias', 'webhook', 'checklist_id']
|
|
399
|
+
for field in optional_string_fields:
|
|
400
|
+
if field in step_data and step_data[field]:
|
|
401
|
+
create_data[field] = str(step_data[field]).strip()
|
|
402
|
+
|
|
403
|
+
# Add optional integer fields
|
|
404
|
+
if 'position' in step_data:
|
|
405
|
+
position = step_data['position']
|
|
406
|
+
if isinstance(position, int) and position > 0:
|
|
407
|
+
create_data['position'] = position
|
|
408
|
+
else:
|
|
409
|
+
raise ValueError("Position must be a positive integer")
|
|
410
|
+
|
|
411
|
+
if 'max_assignable' in step_data:
|
|
412
|
+
max_assignable = step_data['max_assignable']
|
|
413
|
+
if isinstance(max_assignable, int) and max_assignable > 0:
|
|
414
|
+
create_data['max_assignable'] = max_assignable
|
|
415
|
+
elif max_assignable is not None:
|
|
416
|
+
raise ValueError("max_assignable must be a positive integer or None")
|
|
417
|
+
|
|
418
|
+
# Add boolean fields
|
|
419
|
+
boolean_fields = [
|
|
420
|
+
'allow_guest_owners', 'skip_start_process', 'can_complete_only_assignees',
|
|
421
|
+
'everyone_must_complete', 'prevent_guest_comment', 'is_soft_start_date',
|
|
422
|
+
'role_changes_every_time'
|
|
423
|
+
]
|
|
424
|
+
for field in boolean_fields:
|
|
425
|
+
if field in step_data:
|
|
426
|
+
create_data[field] = bool(step_data[field])
|
|
427
|
+
|
|
428
|
+
# Add assignees if provided
|
|
429
|
+
if 'assignees' in step_data and step_data['assignees']:
|
|
430
|
+
assignees_list = step_data['assignees']
|
|
431
|
+
if isinstance(assignees_list, list):
|
|
432
|
+
for user_id in assignees_list:
|
|
433
|
+
if not isinstance(user_id, int):
|
|
434
|
+
raise ValueError(f"User ID {user_id} must be an integer")
|
|
435
|
+
create_data['assignees'] = assignees_list
|
|
436
|
+
else:
|
|
437
|
+
raise ValueError("Assignees must be a list of user IDs")
|
|
438
|
+
|
|
439
|
+
# Add guests if provided
|
|
440
|
+
if 'guests' in step_data and step_data['guests']:
|
|
441
|
+
guests_list = step_data['guests']
|
|
442
|
+
if isinstance(guests_list, list):
|
|
443
|
+
for guest_email in guests_list:
|
|
444
|
+
try:
|
|
445
|
+
validation = validate_email(guest_email)
|
|
446
|
+
# The validated email address
|
|
447
|
+
email = validation.normalized
|
|
448
|
+
except EmailNotValidError as e:
|
|
449
|
+
raise ValueError(f"Invalid email address: {str(e)}")
|
|
450
|
+
create_data['guests'] = guests_list
|
|
451
|
+
else:
|
|
452
|
+
raise ValueError("Guests must be a list of email addresses")
|
|
453
|
+
|
|
454
|
+
# Add roles if provided
|
|
455
|
+
if 'roles' in step_data and step_data['roles']:
|
|
456
|
+
roles_list = step_data['roles']
|
|
457
|
+
if isinstance(roles_list, list):
|
|
458
|
+
create_data['roles'] = [str(role) for role in roles_list]
|
|
459
|
+
else:
|
|
460
|
+
raise ValueError("Roles must be a list of role names")
|
|
461
|
+
|
|
462
|
+
response_data = self.sdk._make_request('POST', endpoint, data=create_data)
|
|
463
|
+
|
|
464
|
+
if isinstance(response_data, dict):
|
|
465
|
+
if 'data' in response_data:
|
|
466
|
+
return response_data['data']
|
|
467
|
+
return response_data
|
|
468
|
+
else:
|
|
469
|
+
self.sdk.logger.warning("Unexpected response format for step creation")
|
|
470
|
+
return {'success': True, 'created_step': create_data}
|
|
471
|
+
|
|
472
|
+
except TallyfyError:
|
|
473
|
+
raise
|
|
474
|
+
except ValueError as e:
|
|
475
|
+
raise TallyfyError(f"Invalid step data: {e}")
|
|
476
|
+
except Exception as e:
|
|
477
|
+
self._handle_api_error(e, "add step to template", org_id=org_id, template_id=template_id)
|