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,469 @@
1
+ """
2
+ Automation management functionality for templates
3
+ """
4
+ from typing import List, Optional, Dict, Any
5
+ from .base import TemplateManagerBase
6
+ from ..models import AutomatedAction, TallyfyError
7
+ from email_validator import validate_email, EmailNotValidError
8
+
9
+
10
+ class TemplateAutomation(TemplateManagerBase):
11
+ """Handles automation rules creation, management, and optimization"""
12
+
13
+ def create_automation_rule(self, org_id: str, template_id: str, rule_data: Dict[str, Any]) -> Dict[str, Any]:
14
+ """
15
+ Create conditional automation (if-then rules).
16
+
17
+ Args:
18
+ org_id: Organization ID
19
+ template_id: Template ID
20
+ rule_data: Dictionary containing automation rule data
21
+
22
+ Returns:
23
+ Dictionary containing created automation rule information
24
+
25
+ Raises:
26
+ TallyfyError: If the request fails
27
+ """
28
+ self._validate_org_id(org_id)
29
+ self._validate_template_id(template_id)
30
+
31
+ try:
32
+ endpoint = f"organizations/{org_id}/checklists/{template_id}/automated_actions"
33
+
34
+ # Validate rule data
35
+ if not isinstance(rule_data, dict):
36
+ raise ValueError("Rule data must be a dictionary")
37
+
38
+ # Build automation data
39
+ automation_data = {}
40
+
41
+ # Add alias if provided
42
+ if 'alias' in rule_data:
43
+ automation_data['automated_alias'] = str(rule_data['alias'])
44
+
45
+ # Add conditions
46
+ if 'conditions' in rule_data and rule_data['conditions']:
47
+ automation_data['conditions'] = rule_data['conditions']
48
+ else:
49
+ raise ValueError("Automation rule must have conditions")
50
+
51
+ # Add actions
52
+ if 'actions' in rule_data and rule_data['actions']:
53
+ automation_data['then_actions'] = rule_data['actions']
54
+ else:
55
+ raise ValueError("Automation rule must have actions")
56
+
57
+ # Add condition logic (AND/OR)
58
+ if 'condition_logic' in rule_data:
59
+ automation_data['condition_logic'] = rule_data['condition_logic']
60
+
61
+ response_data = self.sdk._make_request('POST', endpoint, data=automation_data)
62
+
63
+ if isinstance(response_data, dict):
64
+ if 'data' in response_data:
65
+ return response_data['data']
66
+ return response_data
67
+ else:
68
+ self.sdk.logger.warning("Unexpected response format for automation rule creation")
69
+ return {'success': True, 'created_rule': automation_data}
70
+
71
+ except TallyfyError:
72
+ raise
73
+ except ValueError as e:
74
+ raise TallyfyError(f"Invalid automation rule data: {e}")
75
+ except Exception as e:
76
+ self._handle_api_error(e, "create automation rule", org_id=org_id, template_id=template_id)
77
+
78
+ def update_automation_rule(self, org_id: str, template_id: str, automation_id: str, update_data: Dict[str, Any]) -> Dict[str, Any]:
79
+ """
80
+ Modify automation conditions and actions.
81
+
82
+ Args:
83
+ org_id: Organization ID
84
+ template_id: Template ID
85
+ automation_id: Automation rule ID to update
86
+ update_data: Dictionary containing fields to update
87
+
88
+ Returns:
89
+ Dictionary containing updated automation rule information
90
+
91
+ Raises:
92
+ TallyfyError: If the request fails
93
+ """
94
+ self._validate_org_id(org_id)
95
+ self._validate_template_id(template_id)
96
+
97
+ try:
98
+ endpoint = f"organizations/{org_id}/checklists/{template_id}/automated_actions/{automation_id}"
99
+
100
+ # Validate update data
101
+ if not isinstance(update_data, dict):
102
+ raise ValueError("Update data must be a dictionary")
103
+
104
+ # Build update payload with allowed fields
105
+ allowed_fields = ['automated_alias', 'conditions', 'then_actions', 'condition_logic']
106
+ validated_data = {}
107
+
108
+ for field, value in update_data.items():
109
+ if field in allowed_fields:
110
+ validated_data[field] = value
111
+ elif field == 'alias': # Map alias to automated_alias
112
+ validated_data['automated_alias'] = str(value)
113
+ elif field == 'actions': # Map actions to then_actions
114
+ validated_data['then_actions'] = value
115
+ else:
116
+ self.sdk.logger.warning(f"Ignoring unknown automation field: {field}")
117
+
118
+ if not validated_data:
119
+ raise ValueError("No valid automation fields provided for update")
120
+
121
+ response_data = self.sdk._make_request('PUT', endpoint, data=validated_data)
122
+
123
+ if isinstance(response_data, dict):
124
+ if 'data' in response_data:
125
+ return response_data['data']
126
+ return response_data
127
+ else:
128
+ self.sdk.logger.warning("Unexpected response format for automation rule update")
129
+ return {'success': True, 'updated_rule': validated_data}
130
+
131
+ except TallyfyError:
132
+ raise
133
+ except ValueError as e:
134
+ raise TallyfyError(f"Invalid update data: {e}")
135
+ except Exception as e:
136
+ self._handle_api_error(e, "update automation rule", org_id=org_id, template_id=template_id, automation_id=automation_id)
137
+
138
+ def delete_automation_rule(self, org_id: str, template_id: str, automation_id: str) -> Dict[str, Any]:
139
+ """
140
+ Remove automation rule.
141
+
142
+ Args:
143
+ org_id: Organization ID
144
+ template_id: Template ID
145
+ automation_id: Automation rule ID to delete
146
+
147
+ Returns:
148
+ Dictionary containing deletion confirmation
149
+
150
+ Raises:
151
+ TallyfyError: If the request fails
152
+ """
153
+ self._validate_org_id(org_id)
154
+ self._validate_template_id(template_id)
155
+
156
+ try:
157
+ endpoint = f"organizations/{org_id}/checklists/{template_id}/automated_actions/{automation_id}"
158
+
159
+ response_data = self.sdk._make_request('DELETE', endpoint)
160
+
161
+ if isinstance(response_data, dict):
162
+ return response_data
163
+ else:
164
+ return {'success': True, 'deleted_automation_id': automation_id}
165
+
166
+ except TallyfyError:
167
+ raise
168
+ except Exception as e:
169
+ self._handle_api_error(e, "delete automation rule", org_id=org_id, template_id=template_id, automation_id=automation_id)
170
+
171
+ def consolidate_automation_rules(self, org_id: str, template_id: str, preview_only: bool = True) -> Dict[str, Any]:
172
+ """
173
+ Suggest and implement automation consolidation.
174
+
175
+ Args:
176
+ org_id: Organization ID
177
+ template_id: Template ID
178
+ preview_only: If True, only return suggestions without implementing changes
179
+
180
+ Returns:
181
+ Dictionary containing consolidation analysis and results
182
+
183
+ Raises:
184
+ TallyfyError: If the request fails
185
+ """
186
+ self._validate_org_id(org_id)
187
+ self._validate_template_id(template_id)
188
+
189
+ try:
190
+ # Get automation analysis
191
+ analysis = self.analyze_template_automations(org_id, template_id)
192
+
193
+ consolidation_results = {
194
+ 'template_id': template_id,
195
+ 'preview_only': preview_only,
196
+ 'suggestions': [],
197
+ 'implemented_changes': [],
198
+ 'warnings': [],
199
+ 'summary': {
200
+ 'total_automations': len(analysis.get('automations', [])),
201
+ 'redundant_rules': len(analysis.get('analysis', {}).get('redundant_rules', [])),
202
+ 'conflicting_rules': len(analysis.get('analysis', {}).get('conflicting_rules', [])),
203
+ 'potential_consolidations': 0
204
+ }
205
+ }
206
+
207
+ redundant_rules = analysis.get('analysis', {}).get('redundant_rules', [])
208
+
209
+ # Process redundant rules
210
+ for redundancy in redundant_rules:
211
+ automation_ids = redundancy.get('automation_ids', [])
212
+ similarity_score = redundancy.get('similarity_score', 0)
213
+
214
+ suggestion = {
215
+ 'type': 'consolidation',
216
+ 'automation_ids': automation_ids,
217
+ 'similarity_score': similarity_score,
218
+ 'description': f"Consolidate {len(automation_ids)} similar automation rules",
219
+ 'recommendation': redundancy.get('recommendation', ''),
220
+ 'estimated_effort': 'Low',
221
+ 'impact': 'Reduced complexity and improved maintainability'
222
+ }
223
+
224
+ consolidation_results['suggestions'].append(suggestion)
225
+ consolidation_results['summary']['potential_consolidations'] += 1
226
+
227
+ # If not preview only, implement the consolidation
228
+ if not preview_only:
229
+ try:
230
+ # This is a simplified consolidation - in practice, you'd need more sophisticated logic
231
+ # to merge automation rules properly
232
+ consolidation_results['warnings'].append(
233
+ f"Automatic consolidation of rules {automation_ids} requires manual review"
234
+ )
235
+ except Exception as e:
236
+ consolidation_results['warnings'].append(
237
+ f"Failed to consolidate rules {automation_ids}: {str(e)}"
238
+ )
239
+
240
+ # Check for conflicting rules
241
+ conflicting_rules = analysis.get('analysis', {}).get('conflicting_rules', [])
242
+ for conflict in conflicting_rules:
243
+ consolidation_results['warnings'].append(
244
+ f"Conflicting rules detected: {conflict.get('automation_ids', [])} - manual resolution required"
245
+ )
246
+
247
+ return consolidation_results
248
+
249
+ except TallyfyError:
250
+ raise
251
+ except Exception as e:
252
+ self._handle_api_error(e, "consolidate automation rules", org_id=org_id, template_id=template_id)
253
+
254
+ def add_assignees_to_step(self, org_id: str, template_id: str, step_id: str, assignees: List[int], guests: Optional[List[str]] = None) -> Dict[str, Any]:
255
+ """
256
+ Add assignees to steps (automation-related).
257
+
258
+ Args:
259
+ org_id: Organization ID
260
+ template_id: Template ID
261
+ step_id: Step ID to add assignees to
262
+ assignees: List of user IDs to assign
263
+ guests: Optional list of guest email addresses
264
+
265
+ Returns:
266
+ Dictionary containing updated step information
267
+
268
+ Raises:
269
+ TallyfyError: If the request fails
270
+ """
271
+ self._validate_org_id(org_id)
272
+ self._validate_template_id(template_id)
273
+
274
+ try:
275
+ endpoint = f"organizations/{org_id}/checklists/{template_id}/steps/{step_id}"
276
+
277
+ # Validate assignees
278
+ if not isinstance(assignees, list):
279
+ raise ValueError("Assignees must be a list of user IDs")
280
+
281
+ for user_id in assignees:
282
+ if not isinstance(user_id, int):
283
+ raise ValueError(f"User ID {user_id} must be an integer")
284
+
285
+ # Validate guest emails if provided
286
+ validated_guests = []
287
+ if guests:
288
+ if not isinstance(guests, list):
289
+ raise ValueError("Guests must be a list of email addresses")
290
+
291
+ for guest_email in guests:
292
+ if not isinstance(guest_email, str):
293
+ raise ValueError(f"Guest email {guest_email} must be a string")
294
+ try:
295
+ validation = validate_email(guest_email)
296
+ # The validated email address
297
+ email = validation.normalized
298
+ except EmailNotValidError as e:
299
+ raise ValueError(f"Invalid email address: {str(e)}")
300
+ validated_guests.append(email)
301
+
302
+ # Build update data
303
+ update_data = {
304
+ 'assignees': assignees
305
+ }
306
+
307
+ if validated_guests:
308
+ update_data['guests'] = validated_guests
309
+
310
+ response_data = self.sdk._make_request('PUT', endpoint, data=update_data)
311
+
312
+ if isinstance(response_data, dict):
313
+ if 'data' in response_data:
314
+ return response_data['data']
315
+ return response_data
316
+ else:
317
+ self.sdk.logger.warning("Unexpected response format for assignee update")
318
+ return {
319
+ 'success': True,
320
+ 'step_id': step_id,
321
+ 'added_assignees': assignees,
322
+ 'added_guests': validated_guests
323
+ }
324
+
325
+ except TallyfyError:
326
+ raise
327
+ except ValueError as e:
328
+ raise TallyfyError(f"Invalid assignee data: {e}")
329
+ except Exception as e:
330
+ self._handle_api_error(e, "add assignees to step", org_id=org_id, template_id=template_id, step_id=step_id)
331
+
332
+ def analyze_template_automations(self, org_id: str, template_id: str) -> Dict[str, Any]:
333
+ """
334
+ Analyze automations for conflicts/redundancies.
335
+
336
+ Args:
337
+ org_id: Organization ID
338
+ template_id: Template ID to analyze
339
+
340
+ Returns:
341
+ Dictionary containing comprehensive automation analysis
342
+
343
+ Raises:
344
+ TallyfyError: If the request fails
345
+ """
346
+ self._validate_org_id(org_id)
347
+ self._validate_template_id(template_id)
348
+
349
+ try:
350
+ # Get template with automation data
351
+ template_endpoint = f"organizations/{org_id}/checklists/{template_id}"
352
+ template_params = {'with': 'steps,automated_actions,prerun'}
353
+ template_response = self.sdk._make_request('GET', template_endpoint, params=template_params)
354
+
355
+ template_data = self._extract_data(template_response)
356
+ if not template_data:
357
+ raise TallyfyError("Unable to retrieve template data for automation analysis")
358
+
359
+ automations = template_data.get('automated_actions', [])
360
+ steps = template_data.get('steps', [])
361
+
362
+ # Create step lookup
363
+ step_lookup = {step.get('id'): step.get('title', 'Unknown') for step in steps['data']}
364
+
365
+ analysis = {
366
+ 'redundant_rules': [],
367
+ 'conflicting_rules': [],
368
+ 'complex_rules': [],
369
+ 'simple_rules': [],
370
+ 'statistics': {
371
+ 'total_automations': len(automations),
372
+ 'avg_conditions_per_rule': 0,
373
+ 'avg_actions_per_rule': 0
374
+ }
375
+ }
376
+
377
+ # Analyze each automation
378
+ total_conditions = 0
379
+ total_actions = 0
380
+
381
+ for i, automation in enumerate(automations):
382
+ conditions = automation.get('conditions', [])
383
+ actions = automation.get('actions', [])
384
+
385
+ total_conditions += len(conditions)
386
+ total_actions += len(actions)
387
+
388
+ # Classify by complexity
389
+ if len(conditions) > 3 or len(actions) > 2:
390
+ analysis['complex_rules'].append(automation.get('id'))
391
+ else:
392
+ analysis['simple_rules'].append(automation.get('id'))
393
+
394
+ # Check for redundancy with other automations
395
+ for j, other_automation in enumerate(automations[i+1:], i+1):
396
+ similarity = self._analyze_condition_similarity(
397
+ conditions, other_automation.get('conditions', [])
398
+ )
399
+
400
+ if similarity > 0.8: # High similarity threshold
401
+ analysis['redundant_rules'].append({
402
+ 'automation_ids': [automation.get('id'), other_automation.get('id')],
403
+ 'similarity_score': similarity,
404
+ 'recommendation': 'Consider consolidating these similar rules'
405
+ })
406
+
407
+ # Check for conflicts (same conditions, different actions)
408
+ for j, other_automation in enumerate(automations[i+1:], i+1):
409
+ other_conditions = other_automation.get('conditions', [])
410
+ other_actions = other_automation.get('actions', [])
411
+
412
+ if (self._analyze_condition_similarity(conditions, other_conditions) > 0.9 and
413
+ actions != other_actions):
414
+ analysis['conflicting_rules'].append({
415
+ 'automation_ids': [automation.get('id'), other_automation.get('id')],
416
+ 'description': 'Same conditions with different actions may cause conflicts',
417
+ 'severity': 'high'
418
+ })
419
+
420
+ # Calculate statistics
421
+ if automations:
422
+ analysis['statistics']['avg_conditions_per_rule'] = total_conditions / len(automations)
423
+ analysis['statistics']['avg_actions_per_rule'] = total_actions / len(automations)
424
+
425
+ return {
426
+ 'template_id': template_id,
427
+ 'automations': automations,
428
+ 'analysis': analysis,
429
+ 'step_lookup': step_lookup
430
+ }
431
+
432
+ except TallyfyError:
433
+ raise
434
+ except Exception as e:
435
+ self._handle_api_error(e, "analyze template automations", org_id=org_id, template_id=template_id)
436
+
437
+ def _analyze_condition_similarity(self, conditions1: List[Dict], conditions2: List[Dict]) -> float:
438
+ """
439
+ Helper for condition analysis.
440
+
441
+ Args:
442
+ conditions1: First set of conditions
443
+ conditions2: Second set of conditions
444
+
445
+ Returns:
446
+ Float representing similarity score (0.0 to 1.0)
447
+ """
448
+ if not conditions1 or not conditions2:
449
+ return 0.0
450
+
451
+ # Convert conditions to comparable strings
452
+ set1 = set()
453
+ set2 = set()
454
+
455
+ for condition in conditions1:
456
+ condition_str = f"{condition.get('type', '')}:{condition.get('step_id', '')}:{condition.get('value', '')}"
457
+ set1.add(condition_str)
458
+
459
+ for condition in conditions2:
460
+ condition_str = f"{condition.get('type', '')}:{condition.get('step_id', '')}:{condition.get('value', '')}"
461
+ set2.add(condition_str)
462
+
463
+ if not set1 or not set2:
464
+ return 0.0
465
+
466
+ intersection = set1.intersection(set2)
467
+ union = set1.union(set2)
468
+
469
+ return len(intersection) / len(union)
@@ -0,0 +1,56 @@
1
+ """
2
+ Base class for template management functionality
3
+ """
4
+
5
+ from typing import Any, Optional
6
+ from ..models import TallyfyError
7
+
8
+
9
+ class TemplateManagerBase:
10
+ """Base class providing shared functionality for template management operations"""
11
+
12
+ def __init__(self, sdk):
13
+ """Initialize with SDK instance"""
14
+ self.sdk = sdk
15
+
16
+ def _validate_org_id(self, org_id: str) -> None:
17
+ """Validate organization ID parameter"""
18
+ if not org_id or not isinstance(org_id, str):
19
+ raise ValueError("Organization ID must be a non-empty string")
20
+
21
+ def _validate_template_id(self, template_id: str) -> None:
22
+ """Validate template ID parameter"""
23
+ if not template_id or not isinstance(template_id, str):
24
+ raise ValueError("Template ID must be a non-empty string")
25
+
26
+ def _handle_api_error(self, error: Exception, operation: str, **context) -> None:
27
+ """Common error handling for API operations"""
28
+ error_msg = f"Failed to {operation}"
29
+ if context:
30
+ context_str = ", ".join(f"{k}={v}" for k, v in context.items())
31
+ error_msg += f" ({context_str})"
32
+ error_msg += f": {error}"
33
+
34
+ self.sdk.logger.error(error_msg)
35
+ if isinstance(error, TallyfyError):
36
+ raise
37
+ else:
38
+ raise TallyfyError(error_msg)
39
+
40
+ def _validate_response(self, response: Any, expected_key: Optional[str] = None) -> bool:
41
+ """Validate API response format"""
42
+ if not isinstance(response, dict):
43
+ return False
44
+
45
+ if expected_key and expected_key not in response:
46
+ return False
47
+
48
+ return True
49
+
50
+ def _extract_data(self, response: dict, data_key: str = 'data') -> Any:
51
+ """Extract data from API response with validation"""
52
+ if not self._validate_response(response, data_key):
53
+ self.sdk.logger.warning("Unexpected response format")
54
+ return None
55
+
56
+ return response.get(data_key)