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.

Files changed (34) hide show
  1. tallyfy/__init__.py +8 -4
  2. tallyfy/core.py +8 -8
  3. tallyfy/form_fields_management/__init__.py +70 -0
  4. tallyfy/form_fields_management/base.py +109 -0
  5. tallyfy/form_fields_management/crud_operations.py +234 -0
  6. tallyfy/form_fields_management/options_management.py +222 -0
  7. tallyfy/form_fields_management/suggestions.py +411 -0
  8. tallyfy/task_management/__init__.py +81 -0
  9. tallyfy/task_management/base.py +125 -0
  10. tallyfy/task_management/creation.py +221 -0
  11. tallyfy/task_management/retrieval.py +211 -0
  12. tallyfy/task_management/search.py +196 -0
  13. tallyfy/template_management/__init__.py +85 -0
  14. tallyfy/template_management/analysis.py +1093 -0
  15. tallyfy/template_management/automation.py +469 -0
  16. tallyfy/template_management/base.py +56 -0
  17. tallyfy/template_management/basic_operations.py +477 -0
  18. tallyfy/template_management/health_assessment.py +763 -0
  19. tallyfy/user_management/__init__.py +69 -0
  20. tallyfy/user_management/base.py +146 -0
  21. tallyfy/user_management/invitation.py +286 -0
  22. tallyfy/user_management/retrieval.py +339 -0
  23. {tallyfy-1.0.3.dist-info → tallyfy-1.0.5.dist-info}/METADATA +120 -56
  24. tallyfy-1.0.5.dist-info/RECORD +28 -0
  25. tallyfy/BUILD.md +0 -5
  26. tallyfy/README.md +0 -634
  27. tallyfy/form_fields_management.py +0 -582
  28. tallyfy/task_management.py +0 -356
  29. tallyfy/template_management.py +0 -2607
  30. tallyfy/user_management.py +0 -235
  31. tallyfy-1.0.3.dist-info/RECORD +0 -14
  32. {tallyfy-1.0.3.dist-info → tallyfy-1.0.5.dist-info}/WHEEL +0 -0
  33. {tallyfy-1.0.3.dist-info → tallyfy-1.0.5.dist-info}/licenses/LICENSE +0 -0
  34. {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)