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