tallyfy 1.0.4__py3-none-any.whl → 1.0.6__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 +11 -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/models.py +3 -1
- 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.4.dist-info → tallyfy-1.0.6.dist-info}/METADATA +120 -56
- tallyfy-1.0.6.dist-info/RECORD +28 -0
- tallyfy/BUILD.md +0 -5
- 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.4.dist-info/RECORD +0 -13
- {tallyfy-1.0.4.dist-info → tallyfy-1.0.6.dist-info}/WHEEL +0 -0
- {tallyfy-1.0.4.dist-info → tallyfy-1.0.6.dist-info}/licenses/LICENSE +0 -0
- {tallyfy-1.0.4.dist-info → tallyfy-1.0.6.dist-info}/top_level.txt +0 -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}
|
|
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)
|