tallyfy 1.0.0__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/core.py ADDED
@@ -0,0 +1,333 @@
1
+ """
2
+ Core SDK functionality and base classes
3
+ """
4
+
5
+ import requests
6
+ import time
7
+ import logging
8
+ from typing import Dict, Any, Optional
9
+
10
+ from .models import TallyfyError
11
+
12
+
13
+ class BaseSDK:
14
+ """
15
+ Base SDK class with common functionality for HTTP requests and error handling
16
+ """
17
+
18
+ def __init__(self, api_key: str, base_url: str = "https://api.tallyfy.com", timeout: int = 30, max_retries: int = 3, retry_delay: float = 1.0):
19
+ self.base_url = base_url.rstrip('/')
20
+ self.timeout = timeout
21
+ self.max_retries = max_retries
22
+ self.retry_delay = retry_delay
23
+
24
+ # Setup default headers
25
+ self.default_headers = {
26
+ 'Content-Type': 'application/json',
27
+ 'Accept': 'application/json',
28
+ 'X-Tallyfy-Client': 'TallyfySDK',
29
+ 'Authorization': f'Bearer {api_key}'
30
+ }
31
+
32
+ # Setup logging
33
+ self.logger = logging.getLogger(__name__)
34
+
35
+ # Setup session for connection pooling
36
+ self.session = requests.Session()
37
+ self.session.headers.update(self.default_headers)
38
+
39
+ def _build_url(self, endpoint: str) -> str:
40
+ """Build full URL from endpoint"""
41
+ endpoint = endpoint.lstrip('/')
42
+ return f"{self.base_url}/{endpoint}"
43
+
44
+ def _make_request(self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None, data: Optional[Dict] = None) -> Dict[str, Any]:
45
+ """
46
+ Make HTTP request with retry logic
47
+
48
+ Args:
49
+ method: HTTP method (GET, POST, etc.)
50
+ endpoint: API endpoint
51
+ params: URL parameters
52
+ data: Request body data
53
+
54
+ Returns:
55
+ Parsed JSON response data
56
+
57
+ Raises:
58
+ TallyfyError: If request fails after all retries
59
+ """
60
+ url = self._build_url(endpoint)
61
+
62
+ # Prepare request arguments
63
+ request_args = {
64
+ 'timeout': self.timeout,
65
+ 'params': params
66
+ }
67
+
68
+ if data:
69
+ request_args['json'] = data
70
+
71
+ # Retry logic
72
+ last_exception = None
73
+
74
+ for attempt in range(self.max_retries + 1):
75
+ try:
76
+ self.logger.debug(f"Making {method} request to {url} (attempt {attempt + 1})")
77
+ print(url)
78
+ response = self.session.request(method, url, **request_args)
79
+
80
+ # Parse response
81
+ try:
82
+ response_data = response.json()
83
+ except ValueError:
84
+ response_data = response.text
85
+
86
+ # Check if request was successful
87
+ if response.ok:
88
+ return response_data
89
+ else:
90
+ error_msg = f"API request failed with status {response.status_code}"
91
+ if isinstance(response_data, dict) and 'message' in response_data:
92
+ error_msg += f": {response_data['message']}"
93
+ elif isinstance(response_data, str):
94
+ error_msg += f": {response_data}"
95
+
96
+ # Don't retry on client errors (4xx)
97
+ if 400 <= response.status_code < 500:
98
+ raise TallyfyError(
99
+ error_msg,
100
+ status_code=response.status_code,
101
+ response_data=response_data
102
+ )
103
+
104
+ # Retry on server errors (5xx)
105
+ if attempt < self.max_retries:
106
+ self.logger.warning(f"Request failed, retrying in {self.retry_delay}s...")
107
+ time.sleep(self.retry_delay)
108
+ continue
109
+ else:
110
+ raise TallyfyError(
111
+ error_msg,
112
+ status_code=response.status_code,
113
+ response_data=response_data
114
+ )
115
+
116
+ except requests.exceptions.RequestException as req_error:
117
+ last_exception = req_error
118
+ if attempt < self.max_retries:
119
+ self.logger.warning(f"Request failed with {type(req_error).__name__}, retrying in {self.retry_delay}s...")
120
+ time.sleep(self.retry_delay)
121
+ continue
122
+ else:
123
+ break
124
+
125
+ # If we get here, all retries failed
126
+ raise TallyfyError(f"Request failed after {self.max_retries + 1} attempts: {str(last_exception)}")
127
+
128
+ def close(self):
129
+ """Close the HTTP session and cleanup resources"""
130
+ if hasattr(self, 'session'):
131
+ self.session.close()
132
+
133
+ def __enter__(self):
134
+ """Context manager entry"""
135
+ return self
136
+
137
+ def __exit__(self, exc_type, exc_val, exc_tb):
138
+ """Context manager exit"""
139
+ self.close()
140
+
141
+
142
+ class TallyfySDK(BaseSDK):
143
+ """
144
+ High-level SDK for Tallyfy API endpoints
145
+ Provides typed methods for interacting with Tallyfy's REST API
146
+ """
147
+
148
+ def __init__(self, api_key: str, base_url: str = "https://staging.api.tallyfy.com", timeout: int = 30, max_retries: int = 3, retry_delay: float = 1.0):
149
+ super().__init__(api_key, base_url, timeout, max_retries, retry_delay)
150
+
151
+ # Initialize management modules
152
+ from .user_management import UserManagement
153
+ from .task_management import TaskManagement
154
+ from .template_management import TemplateManagement
155
+ from .form_fields_management import FormFieldManagement
156
+
157
+ self.users = UserManagement(self)
158
+ self.tasks = TaskManagement(self)
159
+ self.templates = TemplateManagement(self)
160
+ self.form_fields = FormFieldManagement(self)
161
+
162
+ # Backward compatibility methods - delegate to management modules
163
+ def get_organization_users(self, org_id: str, with_groups: bool = False):
164
+ """Get all organization members with full profile data."""
165
+ return self.users.get_organization_users(org_id, with_groups)
166
+
167
+ def get_organization_users_list(self, org_id: str):
168
+ """Get organization members with minimal data."""
169
+ return self.users.get_organization_users_list(org_id)
170
+
171
+ def get_organization_guests(self, org_id: str, with_stats: bool = False):
172
+ """Get organization guests with full profile data."""
173
+ return self.users.get_organization_guests(org_id, with_stats)
174
+
175
+ def get_organization_guests_list(self, org_id: str):
176
+ """Get organization guests with minimal data."""
177
+ return self.users.get_organization_guests_list(org_id)
178
+
179
+ def invite_user_to_organization(self, org_id: str, email: str, first_name: str, last_name: str, role: str = "light", message: Optional[str] = None):
180
+ """Invite a member to your organization."""
181
+ return self.users.invite_user_to_organization(org_id, email, first_name, last_name, role, message)
182
+
183
+ def get_my_tasks(self, org_id: str):
184
+ """Get tasks assigned to the current user."""
185
+ return self.tasks.get_my_tasks(org_id)
186
+
187
+ def get_user_tasks(self, org_id: str, user_id: int):
188
+ """Get tasks assigned to a specific user."""
189
+ return self.tasks.get_user_tasks(org_id, user_id)
190
+
191
+ def get_tasks_for_process(self, org_id: str, process_id: Optional[str] = None, process_name: Optional[str] = None):
192
+ """Get all tasks for a given process."""
193
+ return self.tasks.get_tasks_for_process(org_id, process_id, process_name)
194
+
195
+ def get_organization_runs(self, org_id: str, with_data: Optional[str] = None,
196
+ form_fields_values: Optional[bool] = None, owners: Optional[str] = None,
197
+ task_status: Optional[str] = None, groups: Optional[str] = None,
198
+ status: Optional[str] = None, folder: Optional[str] = None,
199
+ checklist_id: Optional[str] = None, starred: Optional[bool] = None,
200
+ run_type: Optional[str] = None, tag: Optional[str] = None):
201
+ """Get all processes (runs) in the organization."""
202
+ return self.tasks.get_organization_runs(org_id, with_data, form_fields_values, owners, task_status, groups, status, folder, checklist_id, starred, run_type, tag)
203
+
204
+ def create_task(self, org_id: str, title: str, deadline: str, description: Optional[str] = None, owners = None, max_assignable: Optional[int] = None, prevent_guest_comment: Optional[bool] = None):
205
+ """Create a standalone task."""
206
+ return self.tasks.create_task(org_id=org_id, title=title, deadline=deadline, description=description, owners=owners, max_assignable=max_assignable, prevent_guest_comment=prevent_guest_comment)
207
+
208
+ def search_processes_by_name(self, org_id: str, process_name: str):
209
+ """Search processes by name."""
210
+ return self.tasks.search_processes_by_name(org_id, process_name)
211
+
212
+ def search_templates_by_name(self, org_id: str, template_name: str):
213
+ """Search templates by name."""
214
+ return self.templates.search_templates_by_name(org_id, template_name)
215
+
216
+ def search(self, org_id: str, search_query: str, search_type: str = "process", per_page: int = 20):
217
+ """Universal search for processes, templates, or tasks."""
218
+ return self.tasks.search(org_id, search_query, search_type, per_page)
219
+
220
+ def get_template(self, org_id: str, template_id: Optional[str] = None, template_name: Optional[str] = None):
221
+ """Get template by ID or name."""
222
+ return self.templates.get_template(org_id, template_id, template_name)
223
+
224
+ def update_template_metadata(self, org_id: str, template_id: str, **kwargs):
225
+ """Update template metadata."""
226
+ return self.templates.update_template_metadata(org_id, template_id, **kwargs)
227
+
228
+ def get_template_with_steps(self, org_id: str, template_id: Optional[str] = None, template_name: Optional[str] = None):
229
+ """Get template with steps."""
230
+ return self.templates.get_template_with_steps(org_id, template_id, template_name)
231
+
232
+ def duplicate_template(self, org_id: str, template_id: str, new_name: str, copy_permissions: bool = False):
233
+ """Duplicate template."""
234
+ return self.templates.duplicate_template(org_id, template_id, new_name, copy_permissions)
235
+
236
+ def get_template_steps(self, org_id: str, template_id: str):
237
+ """Get template steps."""
238
+ return self.templates.get_template_steps(org_id, template_id)
239
+
240
+ def get_step_dependencies(self, org_id: str, template_id: str, step_id: str):
241
+ """Analyze step dependencies."""
242
+ return self.templates.get_step_dependencies(org_id, template_id, step_id)
243
+
244
+ def suggest_step_deadline(self, org_id: str, template_id: str, step_id: str):
245
+ """Suggest step deadline."""
246
+ return self.templates.suggest_step_deadline(org_id, template_id, step_id)
247
+
248
+ # Form field methods
249
+ def add_form_field_to_step(self, org_id: str, template_id: str, step_id: str, field_data: Dict[str, Any]):
250
+ """Add form field to step."""
251
+ return self.form_fields.add_form_field_to_step(org_id, template_id, step_id, field_data)
252
+
253
+ def update_form_field(self, org_id: str, template_id: str, step_id: str, field_id: str, **kwargs):
254
+ """Update form field."""
255
+ return self.form_fields.update_form_field(org_id, template_id, step_id, field_id, **kwargs)
256
+
257
+ def move_form_field(self, org_id: str, template_id: str, from_step: str, field_id: str, to_step: str, position: int = 1):
258
+ """Move form field between steps."""
259
+ return self.form_fields.move_form_field(org_id, template_id, from_step, field_id, to_step, position)
260
+
261
+ def delete_form_field(self, org_id: str, template_id: str, step_id: str, field_id: str):
262
+ """Delete form field."""
263
+ return self.form_fields.delete_form_field(org_id, template_id, step_id, field_id)
264
+
265
+ # Automation management methods
266
+ def create_automation_rule(self, org_id: str, template_id: str, automation_data: Dict[str, Any]):
267
+ """Create conditional automation (if-then rules)."""
268
+ return self.templates.create_automation_rule(org_id, template_id, automation_data)
269
+
270
+ def update_automation_rule(self, org_id: str, template_id: str, automation_id: str, **kwargs):
271
+ """Modify automation conditions and actions."""
272
+ return self.templates.update_automation_rule(org_id, template_id, automation_id, **kwargs)
273
+
274
+ def delete_automation_rule(self, org_id: str, template_id: str, automation_id: str):
275
+ """Remove an automation rule."""
276
+ return self.templates.delete_automation_rule(org_id, template_id, automation_id)
277
+
278
+ def analyze_template_automations(self, org_id: str, template_id: str):
279
+ """Analyze all automations for conflicts, redundancies, and optimization opportunities."""
280
+ return self.templates.analyze_template_automations(org_id, template_id)
281
+
282
+ def consolidate_automation_rules(self, org_id: str, template_id: str, preview: bool = True):
283
+ """Suggest and optionally implement automation consolidation."""
284
+ return self.templates.consolidate_automation_rules(org_id, template_id, preview)
285
+
286
+ def get_step_visibility_conditions(self, org_id: str, template_id: str, step_id: str):
287
+ """Analyze when/how a step becomes visible based on all automations."""
288
+ return self.templates.get_step_visibility_conditions(org_id, template_id, step_id)
289
+
290
+ def suggest_automation_consolidation(self, org_id: str, template_id: str):
291
+ """AI analysis of automation rules with consolidation recommendations."""
292
+ return self.templates.suggest_automation_consolidation(org_id, template_id)
293
+
294
+ # Kickoff field management methods
295
+ # def add_kickoff_field(self, org_id: str, template_id: str, field_data: Dict[str, Any]):
296
+ # """Add kickoff/prerun fields to template."""
297
+ # return self.templates.add_kickoff_field(org_id, template_id, field_data)
298
+
299
+ # def update_kickoff_field(self, org_id: str, template_id: str, field_id: str, **kwargs):
300
+ # """Update kickoff field properties."""
301
+ # return self.templates.update_kickoff_field(org_id, template_id, field_id, **kwargs)
302
+
303
+ def suggest_kickoff_fields(self, org_id: str, template_id: str):
304
+ """Suggest relevant kickoff fields based on template analysis."""
305
+ return self.templates.suggest_kickoff_fields(org_id, template_id)
306
+
307
+ def get_dropdown_options(self, org_id: str, template_id: str, step_id: str, field_id: str):
308
+ """Get dropdown options."""
309
+ return self.form_fields.get_dropdown_options(org_id, template_id, step_id, field_id)
310
+
311
+ def update_dropdown_options(self, org_id: str, template_id: str, step_id: str, field_id: str, options):
312
+ """Update dropdown options."""
313
+ return self.form_fields.update_dropdown_options(org_id, template_id, step_id, field_id, options)
314
+
315
+ def suggest_form_fields_for_step(self, org_id: str, template_id: str, step_id: str):
316
+ """AI-powered form field suggestions."""
317
+ return self.form_fields.suggest_form_fields_for_step(org_id, template_id, step_id)
318
+
319
+ def assess_template_health(self, org_id: str, template_id: str):
320
+ """Comprehensive template health check analyzing multiple aspects."""
321
+ return self.templates.assess_template_health(org_id, template_id)
322
+
323
+ def add_assignees_to_step(self, org_id: str, template_id: str, step_id: str, assignees: Dict[str, Any]):
324
+ """Add assignees to a specific step in a template."""
325
+ return self.templates.add_assignees_to_step(org_id, template_id, step_id, assignees)
326
+
327
+ def edit_description_on_step(self, org_id: str, template_id: str, step_id: str, description: str):
328
+ """Edit the description/summary of a specific step in a template."""
329
+ return self.templates.edit_description_on_step(org_id, template_id, step_id, description)
330
+
331
+ def add_step_to_template(self, org_id: str, template_id: str, step_data: Dict[str, Any]):
332
+ """Add a new step to a template."""
333
+ return self.templates.add_step_to_template(org_id, template_id, step_data)