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,26 @@
1
+ """
2
+ Organization management module for Tallyfy SDK
3
+ """
4
+
5
+ from .base import OrganizationManagerBase
6
+ from .retrieval import OrganizationRetrieval
7
+
8
+ class OrganizationManager(OrganizationRetrieval):
9
+ """
10
+ Complete organization management interface combining all functionality.
11
+
12
+ This class provides access to:
13
+ - Organization retrieval operations
14
+
15
+ Example:
16
+ >>> from tallyfy import TallyfySDK
17
+ >>> sdk = TallyfySDK(api_key="your_api_key")
18
+ >>> orgs = sdk.organizations.get_current_user_organizations()
19
+ >>> print(f"User belongs to {len(orgs.data)} organizations")
20
+ """
21
+ pass
22
+
23
+ # Maintain backward compatibility
24
+ OrganizationManagement = OrganizationManager
25
+
26
+ __all__ = ['OrganizationManager', 'OrganizationManagement', 'OrganizationRetrieval', 'OrganizationManagerBase']
@@ -0,0 +1,76 @@
1
+ """
2
+ Base class for organization management operations
3
+ """
4
+
5
+ import logging
6
+ from typing import Dict, Any, List, Optional
7
+ from ..models import TallyfyError
8
+
9
+
10
+ class OrganizationManagerBase:
11
+ """Base class providing common functionality for organization management operations"""
12
+
13
+ def __init__(self, sdk):
14
+ """
15
+ Initialize the organization manager base.
16
+
17
+ Args:
18
+ sdk: The main Tallyfy SDK instance
19
+ """
20
+ self.sdk = sdk
21
+ self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
22
+
23
+ def _extract_data(self, response_data: Dict[str, Any], default_empty: bool = True) -> Any:
24
+ """
25
+ Extract data from API response.
26
+
27
+ Args:
28
+ response_data: Raw API response
29
+ default_empty: Return empty list if no data found
30
+
31
+ Returns:
32
+ Extracted data
33
+ """
34
+ if not response_data:
35
+ return [] if default_empty else None
36
+
37
+ # Handle direct data responses
38
+ if 'data' in response_data:
39
+ return response_data['data']
40
+
41
+ # Handle responses where the main content is directly available
42
+ # This is common for single item responses
43
+ return response_data if not default_empty else []
44
+
45
+ def _handle_api_error(self, error: Exception, operation: str, **context) -> None:
46
+ """
47
+ Handle and log API errors consistently.
48
+
49
+ Args:
50
+ error: The exception that occurred
51
+ operation: Description of the operation that failed
52
+ **context: Additional context for debugging
53
+ """
54
+ context_str = ", ".join([f"{k}={v}" for k, v in context.items()])
55
+ error_message = f"Failed to {operation}"
56
+ if context_str:
57
+ error_message += f" ({context_str})"
58
+
59
+ self.logger.error(f"{error_message}: {str(error)}")
60
+
61
+ if isinstance(error, TallyfyError):
62
+ raise error
63
+ else:
64
+ raise TallyfyError(f"{error_message}: {str(error)}")
65
+
66
+ def _build_query_params(self, **kwargs) -> Dict[str, Any]:
67
+ """
68
+ Build query parameters for API requests.
69
+
70
+ Args:
71
+ **kwargs: Parameters to include in the query string
72
+
73
+ Returns:
74
+ Dictionary of query parameters with None values filtered out
75
+ """
76
+ return {key: value for key, value in kwargs.items() if value is not None}
@@ -0,0 +1,39 @@
1
+ """
2
+ Organization retrieval operations
3
+ """
4
+
5
+ from typing import List, Optional
6
+ from .base import OrganizationManagerBase
7
+ from ..models import Organization, OrganizationsList, TallyfyError
8
+
9
+
10
+ class OrganizationRetrieval(OrganizationManagerBase):
11
+ """Handles organization retrieval operations"""
12
+
13
+ def get_current_user_organizations(self, page: int = 1, per_page: int = 10) -> OrganizationsList:
14
+ """
15
+ Get all organizations the current member is a part of.
16
+
17
+ Args:
18
+ page: Page number (default: 1)
19
+ per_page: Number of results per page (default: 10, max: 100)
20
+
21
+ Returns:
22
+ OrganizationsList object containing organizations and pagination metadata
23
+
24
+ Raises:
25
+ TallyfyError: If the request fails
26
+ """
27
+ try:
28
+ endpoint = "me/organizations"
29
+ params = self._build_query_params(page=page, per_page=min(per_page, 100))
30
+
31
+ response_data = self.sdk._make_request('GET', endpoint, params=params)
32
+
33
+ # Return the structured response with pagination
34
+ return OrganizationsList.from_dict(response_data)
35
+
36
+ except TallyfyError:
37
+ raise
38
+ except Exception as e:
39
+ self._handle_api_error(e, "get my organizations", page=page, per_page=per_page)
@@ -0,0 +1,81 @@
1
+ """
2
+ Task Management Package
3
+
4
+ This package provides a refactored, modular approach to task management
5
+ functionality, breaking down the monolithic TaskManagement class into
6
+ specialized components for better maintainability and separation of concerns.
7
+
8
+ Classes:
9
+ TaskRetrieval: Task and process retrieval operations
10
+ TaskSearch: Search operations for tasks, processes, and templates
11
+ TaskCreation: Task creation operations
12
+ TaskManager: Unified interface combining all functionality
13
+ """
14
+
15
+ from .base import TaskManagerBase
16
+ from .retrieval import TaskRetrieval
17
+ from .search import TaskSearch
18
+ from .creation import TaskCreation
19
+
20
+
21
+ class TaskManager:
22
+ """
23
+ Unified interface for task management functionality.
24
+
25
+ This class provides access to all task management capabilities
26
+ through a single interface while maintaining the modular structure
27
+ underneath.
28
+ """
29
+
30
+ def __init__(self, sdk):
31
+ """
32
+ Initialize task manager with SDK instance.
33
+
34
+ Args:
35
+ sdk: Main SDK instance
36
+ """
37
+ self.retrieval = TaskRetrieval(sdk)
38
+ self._search_service = TaskSearch(sdk) # Use private name to avoid conflict
39
+ self.creation = TaskCreation(sdk)
40
+
41
+ # For backward compatibility, expose common methods at the top level
42
+
43
+ # Retrieval methods
44
+ self.get_my_tasks = self.retrieval.get_my_tasks
45
+ self.get_user_tasks = self.retrieval.get_user_tasks
46
+ self.get_tasks_for_process = self.retrieval.get_tasks_for_process
47
+ self.get_organization_runs = self.retrieval.get_organization_runs
48
+ self.get_organization_processes = self.retrieval.get_organization_processes
49
+
50
+ # Search methods
51
+ self.search_processes_by_name = self._search_service.search_processes_by_name
52
+ self.search = self._search_service.search # Main search method for backward compatibility
53
+ self.search_processes = self._search_service.search_processes
54
+ self.search_templates = self._search_service.search_templates
55
+ self.search_tasks = self._search_service.search_tasks
56
+ self.find_process_by_name = self._search_service.find_process_by_name
57
+
58
+ # Creation methods
59
+ self.create_task = self.creation.create_task
60
+ self.create_simple_task = self.creation.create_simple_task
61
+ self.create_user_task = self.creation.create_user_task
62
+ self.create_guest_task = self.creation.create_guest_task
63
+ self.create_group_task = self.creation.create_group_task
64
+
65
+ @property
66
+ def search_service(self):
67
+ """Access to the TaskSearch service object for advanced search operations."""
68
+ return self._search_service
69
+
70
+
71
+ # For backward compatibility, create an alias
72
+ TaskManagement = TaskManager
73
+
74
+ __all__ = [
75
+ 'TaskManagerBase',
76
+ 'TaskRetrieval',
77
+ 'TaskSearch',
78
+ 'TaskCreation',
79
+ 'TaskManager',
80
+ 'TaskManagement' # Backward compatibility alias
81
+ ]
@@ -0,0 +1,125 @@
1
+ """
2
+ Base class for task management operations
3
+ """
4
+
5
+ from typing import Optional, List, Dict, Any
6
+ from ..models import TallyfyError
7
+
8
+
9
+ class TaskManagerBase:
10
+ """Base class providing common functionality for task management"""
11
+
12
+ def __init__(self, sdk):
13
+ """
14
+ Initialize base task manager.
15
+
16
+ Args:
17
+ sdk: Main SDK instance
18
+ """
19
+ self.sdk = sdk
20
+
21
+ def _validate_org_id(self, org_id: str) -> None:
22
+ """
23
+ Validate organization ID parameter.
24
+
25
+ Args:
26
+ org_id: Organization ID to validate
27
+
28
+ Raises:
29
+ ValueError: If org_id is invalid
30
+ """
31
+ if not org_id or not isinstance(org_id, str):
32
+ raise ValueError("Organization ID must be a non-empty string")
33
+
34
+ def _validate_user_id(self, user_id: int) -> None:
35
+ """
36
+ Validate user ID parameter.
37
+
38
+ Args:
39
+ user_id: User ID to validate
40
+
41
+ Raises:
42
+ ValueError: If user_id is invalid
43
+ """
44
+ if not isinstance(user_id, int) or user_id <= 0:
45
+ raise ValueError("User ID must be a positive integer")
46
+
47
+ def _validate_process_id(self, process_id: str) -> None:
48
+ """
49
+ Validate process ID parameter.
50
+
51
+ Args:
52
+ process_id: Process ID to validate
53
+
54
+ Raises:
55
+ ValueError: If process_id is invalid
56
+ """
57
+ if not process_id or not isinstance(process_id, str):
58
+ raise ValueError("Process ID must be a non-empty string")
59
+
60
+ def _extract_data(self, response_data, data_key: Optional[str] = None) -> List[Dict[str, Any]]:
61
+ """
62
+ Extract data from API response.
63
+
64
+ Args:
65
+ response_data: Raw response from API
66
+ data_key: Optional key to extract from response (e.g., 'process', 'blueprint')
67
+
68
+ Returns:
69
+ Extracted data list or empty list
70
+ """
71
+ if isinstance(response_data, dict):
72
+ if data_key and data_key in response_data:
73
+ # Handle search responses with specific keys
74
+ nested_data = response_data[data_key]
75
+ if isinstance(nested_data, dict) and 'data' in nested_data:
76
+ return nested_data['data']
77
+ elif isinstance(nested_data, list):
78
+ return nested_data
79
+ elif 'data' in response_data:
80
+ return response_data['data']
81
+ return []
82
+ elif isinstance(response_data, list):
83
+ return response_data
84
+ return []
85
+
86
+ def _handle_api_error(self, error: Exception, operation: str, **context) -> None:
87
+ """
88
+ Handle API errors with context.
89
+
90
+ Args:
91
+ error: The exception that occurred
92
+ operation: Description of the operation that failed
93
+ **context: Additional context for error logging
94
+ """
95
+ context_str = ", ".join([f"{k}={v}" for k, v in context.items()])
96
+ error_msg = f"Failed to {operation}"
97
+ if context_str:
98
+ error_msg += f" ({context_str})"
99
+ error_msg += f": {error}"
100
+
101
+ self.sdk.logger.error(error_msg)
102
+
103
+ if isinstance(error, TallyfyError):
104
+ raise error
105
+ else:
106
+ raise TallyfyError(error_msg)
107
+
108
+ def _build_query_params(self, **kwargs) -> Dict[str, Any]:
109
+ """
110
+ Build query parameters from keyword arguments, filtering out None values.
111
+
112
+ Args:
113
+ **kwargs: Keyword arguments to convert to query parameters
114
+
115
+ Returns:
116
+ Dictionary of non-None parameters
117
+ """
118
+ params = {}
119
+ for key, value in kwargs.items():
120
+ if value is not None:
121
+ if isinstance(value, bool):
122
+ params[key] = 'true' if value else 'false'
123
+ else:
124
+ params[key] = str(value)
125
+ return params
@@ -0,0 +1,221 @@
1
+ """
2
+ Task creation operations
3
+ """
4
+
5
+ from typing import Optional
6
+ from .base import TaskManagerBase
7
+ from ..models import Task, TaskOwners, TallyfyError
8
+ from email_validator import validate_email, EmailNotValidError
9
+
10
+
11
+ class TaskCreation(TaskManagerBase):
12
+ """Handles task creation operations"""
13
+
14
+ def create_task(self, org_id: str, title: str, deadline: str,
15
+ owners: TaskOwners, description: Optional[str] = None,
16
+ max_assignable: Optional[int] = None, prevent_guest_comment: Optional[bool] = None) -> Optional[Task]:
17
+ """
18
+ Create a standalone task in the organization.
19
+
20
+ Args:
21
+ org_id: Organization ID
22
+ title: Task name (required)
23
+ deadline: Task deadline in "YYYY-mm-dd HH:ii:ss" format
24
+ owners: TaskOwners object with users, guests, and groups
25
+ description: Task description (optional)
26
+ max_assignable: Maximum number of assignees (optional)
27
+ prevent_guest_comment: Prevent guests from commenting (optional)
28
+
29
+ Returns:
30
+ Task object for the created task
31
+
32
+ Raises:
33
+ TallyfyError: If the request fails
34
+ ValueError: If required parameters are missing or invalid
35
+ """
36
+ self._validate_org_id(org_id)
37
+
38
+ if not title or not isinstance(title, str):
39
+ raise ValueError("Task title must be a non-empty string")
40
+
41
+ if not deadline or not isinstance(deadline, str):
42
+ raise ValueError("Task deadline must be a non-empty string in 'YYYY-mm-dd HH:ii:ss' format")
43
+
44
+ # Validate that at least one assignee is provided
45
+ if not owners or not isinstance(owners, TaskOwners):
46
+ raise ValueError("TaskOwners object is required")
47
+
48
+ if not owners.users and not owners.guests and not owners.groups:
49
+ raise ValueError("At least one assignee is required (users, guests, or groups)")
50
+
51
+ # Validate max_assignable if provided
52
+ if max_assignable is not None and (not isinstance(max_assignable, int) or max_assignable <= 0):
53
+ raise ValueError("max_assignable must be a positive integer")
54
+
55
+ try:
56
+ endpoint = f"organizations/{org_id}/tasks"
57
+
58
+ task_data = {
59
+ "title": title,
60
+ "deadline": deadline,
61
+ "owners": {
62
+ "users": owners.users or [],
63
+ "guests": owners.guests or [],
64
+ "groups": owners.groups or []
65
+ },
66
+ "task_type": "task",
67
+ "separate_task_for_each_assignee": True,
68
+ "status": "not-started",
69
+ "everyone_must_complete": False,
70
+ "is_soft_start_date": True
71
+ }
72
+
73
+ # Add optional fields
74
+ if description:
75
+ task_data["description"] = description
76
+ if max_assignable is not None:
77
+ task_data["max_assignable"] = max_assignable
78
+ if prevent_guest_comment is not None:
79
+ task_data["prevent_guest_comment"] = prevent_guest_comment
80
+
81
+ response_data = self.sdk._make_request('POST', endpoint, data=task_data)
82
+
83
+ task_response_data = self._extract_data(response_data)
84
+ if task_response_data:
85
+ # Handle both single task and list responses
86
+ if isinstance(task_response_data, list) and task_response_data:
87
+ return Task.from_dict(task_response_data[0])
88
+ elif isinstance(task_response_data, dict):
89
+ return Task.from_dict(task_response_data)
90
+
91
+ # Check if response_data itself is the task data
92
+ if isinstance(response_data, dict) and 'data' in response_data:
93
+ task_data_response = response_data['data']
94
+ if isinstance(task_data_response, dict):
95
+ return Task.from_dict(task_data_response)
96
+
97
+ self.sdk.logger.warning("Unexpected response format for task creation")
98
+ return None
99
+
100
+ except TallyfyError:
101
+ raise
102
+ except Exception as e:
103
+ self._handle_api_error(e, "create task", org_id=org_id, title=title)
104
+
105
+ def create_simple_task(self, org_id: str, title: str, deadline: str, user_ids: Optional[list] = None,
106
+ guest_emails: Optional[list] = None, group_ids: Optional[list] = None,
107
+ description: Optional[str] = None) -> Optional[Task]:
108
+ """
109
+ Create a simple task with basic assignee information.
110
+
111
+ This is a convenience method that creates TaskOwners internally.
112
+
113
+ Args:
114
+ org_id: Organization ID
115
+ title: Task name (required)
116
+ deadline: Task deadline in "YYYY-mm-dd HH:ii:ss" format
117
+ user_ids: List of user IDs to assign (optional)
118
+ guest_emails: List of guest email addresses to assign (optional)
119
+ group_ids: List of group IDs to assign (optional)
120
+ description: Task description (optional)
121
+
122
+ Returns:
123
+ Task object for the created task
124
+
125
+ Raises:
126
+ TallyfyError: If the request fails
127
+ ValueError: If required parameters are missing or no assignees provided
128
+ """
129
+ # Validate that at least one assignee type is provided
130
+ if not user_ids and not guest_emails and not group_ids:
131
+ raise ValueError("At least one assignee is required (user_ids, guest_emails, or group_ids)")
132
+
133
+ # Create TaskOwners object
134
+ owners = TaskOwners(
135
+ users=user_ids or [],
136
+ guests=guest_emails or [],
137
+ groups=group_ids or []
138
+ )
139
+
140
+ return self.create_task(org_id, title, deadline, owners, description)
141
+
142
+ def create_user_task(self, org_id: str, title: str, deadline: str, user_ids: list,
143
+ description: Optional[str] = None) -> Optional[Task]:
144
+ """
145
+ Create a task assigned to specific users only.
146
+
147
+ Args:
148
+ org_id: Organization ID
149
+ title: Task name (required)
150
+ deadline: Task deadline in "YYYY-mm-dd HH:ii:ss" format
151
+ user_ids: List of user IDs to assign (required)
152
+ description: Task description (optional)
153
+
154
+ Returns:
155
+ Task object for the created task
156
+
157
+ Raises:
158
+ TallyfyError: If the request fails
159
+ ValueError: If required parameters are missing
160
+ """
161
+ if not user_ids or not isinstance(user_ids, list):
162
+ raise ValueError("user_ids must be a non-empty list")
163
+
164
+ return self.create_simple_task(org_id, title, deadline, user_ids=user_ids, description=description)
165
+
166
+ def create_guest_task(self, org_id: str, title: str, deadline: str, guest_emails: list,
167
+ description: Optional[str] = None) -> Optional[Task]:
168
+ """
169
+ Create a task assigned to guests only.
170
+
171
+ Args:
172
+ org_id: Organization ID
173
+ title: Task name (required)
174
+ deadline: Task deadline in "YYYY-mm-dd HH:ii:ss" format
175
+ guest_emails: List of guest email addresses to assign (required)
176
+ description: Task description (optional)
177
+
178
+ Returns:
179
+ Task object for the created task
180
+
181
+ Raises:
182
+ TallyfyError: If the request fails
183
+ ValueError: If required parameters are missing
184
+ """
185
+ if not guest_emails or not isinstance(guest_emails, list):
186
+ raise ValueError("guest_emails must be a non-empty list")
187
+
188
+ # Basic email validation
189
+ for email in guest_emails:
190
+ try:
191
+ validation = validate_email(email)
192
+ # The validated email address
193
+ email = validation.normalized
194
+ except EmailNotValidError as e:
195
+ raise ValueError(f"Invalid email address: {str(e)}")
196
+
197
+ return self.create_simple_task(org_id, title, deadline, guest_emails=guest_emails, description=description)
198
+
199
+ def create_group_task(self, org_id: str, title: str, deadline: str, group_ids: list,
200
+ description: Optional[str] = None) -> Optional[Task]:
201
+ """
202
+ Create a task assigned to groups only.
203
+
204
+ Args:
205
+ org_id: Organization ID
206
+ title: Task name (required)
207
+ deadline: Task deadline in "YYYY-mm-dd HH:ii:ss" format
208
+ group_ids: List of group IDs to assign (required)
209
+ description: Task description (optional)
210
+
211
+ Returns:
212
+ Task object for the created task
213
+
214
+ Raises:
215
+ TallyfyError: If the request fails
216
+ ValueError: If required parameters are missing
217
+ """
218
+ if not group_ids or not isinstance(group_ids, list):
219
+ raise ValueError("group_ids must be a non-empty list")
220
+
221
+ return self.create_simple_task(org_id, title, deadline, group_ids=group_ids, description=description)