tallyfy 1.0.4__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 (33) 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.4.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/form_fields_management.py +0 -582
  27. tallyfy/task_management.py +0 -356
  28. tallyfy/template_management.py +0 -2607
  29. tallyfy/user_management.py +0 -235
  30. tallyfy-1.0.4.dist-info/RECORD +0 -13
  31. {tallyfy-1.0.4.dist-info → tallyfy-1.0.5.dist-info}/WHEEL +0 -0
  32. {tallyfy-1.0.4.dist-info → tallyfy-1.0.5.dist-info}/licenses/LICENSE +0 -0
  33. {tallyfy-1.0.4.dist-info → tallyfy-1.0.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,69 @@
1
+ """
2
+ User Management Package
3
+
4
+ This package provides a refactored, modular approach to user management
5
+ functionality, breaking down the monolithic UserManagement class into
6
+ specialized components for better maintainability and separation of concerns.
7
+
8
+ Classes:
9
+ UserRetrieval: User and guest retrieval operations
10
+ UserInvitation: User invitation operations
11
+ UserManager: Unified interface combining all functionality
12
+ """
13
+
14
+ from .base import UserManagerBase
15
+ from .retrieval import UserRetrieval
16
+ from .invitation import UserInvitation
17
+
18
+
19
+ class UserManager:
20
+ """
21
+ Unified interface for user management functionality.
22
+
23
+ This class provides access to all user management capabilities
24
+ through a single interface while maintaining the modular structure
25
+ underneath.
26
+ """
27
+
28
+ def __init__(self, sdk):
29
+ """
30
+ Initialize user manager with SDK instance.
31
+
32
+ Args:
33
+ sdk: Main SDK instance
34
+ """
35
+ self.retrieval = UserRetrieval(sdk)
36
+ self.invitation = UserInvitation(sdk)
37
+
38
+ # For backward compatibility, expose common methods at the top level
39
+
40
+ # Retrieval methods
41
+ self.get_current_user_info = self.retrieval.get_current_user_info
42
+ self.get_organization_users = self.retrieval.get_organization_users
43
+ self.get_organization_users_list = self.retrieval.get_organization_users_list
44
+ self.get_organization_guests = self.retrieval.get_organization_guests
45
+ self.get_organization_guests_list = self.retrieval.get_organization_guests_list
46
+ self.get_all_organization_members = self.retrieval.get_all_organization_members
47
+ self.get_user_by_email = self.retrieval.get_user_by_email
48
+ self.get_guest_by_email = self.retrieval.get_guest_by_email
49
+ self.search_members_by_name = self.retrieval.search_members_by_name
50
+
51
+ # Invitation methods
52
+ self.invite_user_to_organization = self.invitation.invite_user_to_organization
53
+ self.invite_multiple_users = self.invitation.invite_multiple_users
54
+ self.resend_invitation = self.invitation.resend_invitation
55
+ self.invite_user_with_custom_role_permissions = self.invitation.invite_user_with_custom_role_permissions
56
+ self.get_invitation_template_message = self.invitation.get_invitation_template_message
57
+ self.validate_invitation_data = self.invitation.validate_invitation_data
58
+
59
+
60
+ # For backward compatibility, create an alias
61
+ UserManagement = UserManager
62
+
63
+ __all__ = [
64
+ 'UserManagerBase',
65
+ 'UserRetrieval',
66
+ 'UserInvitation',
67
+ 'UserManager',
68
+ 'UserManagement' # Backward compatibility alias
69
+ ]
@@ -0,0 +1,146 @@
1
+ """
2
+ Base class for user management operations
3
+ """
4
+
5
+ from typing import Optional, List, Dict, Any
6
+ from ..models import TallyfyError
7
+ from email_validator import validate_email, EmailNotValidError
8
+
9
+
10
+ class UserManagerBase:
11
+ """Base class providing common functionality for user management"""
12
+
13
+ def __init__(self, sdk):
14
+ """
15
+ Initialize base user manager.
16
+
17
+ Args:
18
+ sdk: Main SDK instance
19
+ """
20
+ self.sdk = sdk
21
+
22
+ def _validate_org_id(self, org_id: str) -> None:
23
+ """
24
+ Validate organization ID parameter.
25
+
26
+ Args:
27
+ org_id: Organization ID to validate
28
+
29
+ Raises:
30
+ ValueError: If org_id is invalid
31
+ """
32
+ if not org_id or not isinstance(org_id, str):
33
+ raise ValueError("Organization ID must be a non-empty string")
34
+
35
+ def _validate_email(self, email: str) -> None:
36
+ """
37
+ Validate email parameter.
38
+
39
+ Args:
40
+ email: Email to validate
41
+
42
+ Raises:
43
+ ValueError: If email is invalid
44
+ """
45
+ if not email or not isinstance(email, str):
46
+ raise ValueError("Email must be a non-empty string")
47
+
48
+ try:
49
+ validation = validate_email(email)
50
+ # The validated email address
51
+ email = validation.normalized
52
+ except EmailNotValidError as e:
53
+ raise ValueError(f"Invalid email address: {str(e)}")
54
+ # # Basic email validation
55
+ # import re
56
+ # email_pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
57
+ # if not re.match(email_pattern, email):
58
+ # raise ValueError(f"Invalid email address: {email}")
59
+
60
+ def _validate_name(self, name: str, field_name: str) -> None:
61
+ """
62
+ Validate name parameter.
63
+
64
+ Args:
65
+ name: Name to validate
66
+ field_name: Name of the field for error messages
67
+
68
+ Raises:
69
+ ValueError: If name is invalid
70
+ """
71
+ if not name or not isinstance(name, str) or not name.strip():
72
+ raise ValueError(f"{field_name} must be a non-empty string")
73
+
74
+ def _validate_role(self, role: str) -> None:
75
+ """
76
+ Validate user role parameter.
77
+
78
+ Args:
79
+ role: Role to validate
80
+
81
+ Raises:
82
+ ValueError: If role is invalid
83
+ """
84
+ valid_roles = ["light", "standard", "admin"]
85
+ if role not in valid_roles:
86
+ raise ValueError(f"Role must be one of: {', '.join(valid_roles)}")
87
+
88
+ def _extract_data(self, response_data, default_empty: bool = True) -> List[Dict[str, Any]]:
89
+ """
90
+ Extract data from API response.
91
+
92
+ Args:
93
+ response_data: Raw response from API
94
+ default_empty: Return empty list if no data found
95
+
96
+ Returns:
97
+ Extracted data list or single item, or empty list/None
98
+ """
99
+ if isinstance(response_data, dict):
100
+ if 'data' in response_data:
101
+ return response_data['data']
102
+ return response_data if not default_empty else []
103
+ elif isinstance(response_data, list):
104
+ return response_data
105
+ return [] if default_empty else None
106
+
107
+ def _handle_api_error(self, error: Exception, operation: str, **context) -> None:
108
+ """
109
+ Handle API errors with context.
110
+
111
+ Args:
112
+ error: The exception that occurred
113
+ operation: Description of the operation that failed
114
+ **context: Additional context for error logging
115
+ """
116
+ context_str = ", ".join([f"{k}={v}" for k, v in context.items()])
117
+ error_msg = f"Failed to {operation}"
118
+ if context_str:
119
+ error_msg += f" ({context_str})"
120
+ error_msg += f": {error}"
121
+
122
+ self.sdk.logger.error(error_msg)
123
+
124
+ if isinstance(error, TallyfyError):
125
+ raise error
126
+ else:
127
+ raise TallyfyError(error_msg)
128
+
129
+ def _build_query_params(self, **kwargs) -> Dict[str, Any]:
130
+ """
131
+ Build query parameters from keyword arguments, filtering out None values.
132
+
133
+ Args:
134
+ **kwargs: Keyword arguments to convert to query parameters
135
+
136
+ Returns:
137
+ Dictionary of non-None parameters
138
+ """
139
+ params = {}
140
+ for key, value in kwargs.items():
141
+ if value is not None:
142
+ if isinstance(value, bool):
143
+ params[key] = 'true' if value else 'false'
144
+ else:
145
+ params[key] = str(value)
146
+ return params
@@ -0,0 +1,286 @@
1
+ """
2
+ User invitation operations
3
+ """
4
+
5
+ from typing import List, Optional
6
+ from .base import UserManagerBase
7
+ from ..models import User, TallyfyError
8
+
9
+
10
+ class UserInvitation(UserManagerBase):
11
+ """Handles user invitation operations"""
12
+
13
+ def invite_user_to_organization(self, org_id: str, email: str, first_name: str, last_name: str,
14
+ role: str = "light", message: Optional[str] = None) -> Optional[User]:
15
+ """
16
+ Invite a member to your organization.
17
+
18
+ Args:
19
+ org_id: Organization ID
20
+ email: Email address of the user to invite
21
+ first_name: First name of the user (required)
22
+ last_name: Last name of the user (required)
23
+ role: User role - 'light', 'standard', or 'admin' (default: 'light')
24
+ message: Custom invitation message (optional)
25
+
26
+ Returns:
27
+ User object for the invited user
28
+
29
+ Raises:
30
+ TallyfyError: If the request fails
31
+ ValueError: If parameters are invalid
32
+ """
33
+ # Validate inputs
34
+ self._validate_org_id(org_id)
35
+ self._validate_email(email)
36
+ self._validate_name(first_name, "First name")
37
+ self._validate_name(last_name, "Last name")
38
+ self._validate_role(role)
39
+
40
+ try:
41
+ endpoint = f"organizations/{org_id}/users/invite"
42
+
43
+ invite_data = {
44
+ "email": email,
45
+ "first_name": first_name.strip(),
46
+ "last_name": last_name.strip(),
47
+ "role": role
48
+ }
49
+
50
+ # Add message if provided, otherwise use default
51
+ if message:
52
+ invite_data["message"] = message
53
+ else:
54
+ invite_data["message"] = "Please join Tallyfy - it's going to help us automate tasks between people."
55
+
56
+ response_data = self.sdk._make_request('POST', endpoint, data=invite_data)
57
+
58
+ user_data = self._extract_data(response_data, default_empty=False)
59
+ if user_data:
60
+ if isinstance(user_data, dict):
61
+ return User.from_dict(user_data)
62
+ elif isinstance(user_data, list) and user_data:
63
+ return User.from_dict(user_data[0])
64
+
65
+ self.sdk.logger.warning("Unexpected response format for user invitation")
66
+ return None
67
+
68
+ except TallyfyError:
69
+ raise
70
+ except Exception as e:
71
+ self._handle_api_error(e, "invite user to organization", org_id=org_id, email=email)
72
+
73
+ def invite_multiple_users(self, org_id: str, invitations: List[dict],
74
+ default_role: str = "light", default_message: Optional[str] = None) -> List[Optional[User]]:
75
+ """
76
+ Invite multiple users to the organization in batch.
77
+
78
+ Args:
79
+ org_id: Organization ID
80
+ invitations: List of invitation dictionaries, each containing:
81
+ - email (required)
82
+ - first_name (required)
83
+ - last_name (required)
84
+ - role (optional, defaults to default_role)
85
+ - message (optional, defaults to default_message)
86
+ default_role: Default role for users where role is not specified
87
+ default_message: Default message for users where message is not specified
88
+
89
+ Returns:
90
+ List of User objects for successfully invited users (None for failed invitations)
91
+
92
+ Raises:
93
+ TallyfyError: If any invitation fails
94
+ ValueError: If parameters are invalid
95
+ """
96
+ self._validate_org_id(org_id)
97
+ self._validate_role(default_role)
98
+
99
+ if not invitations or not isinstance(invitations, list):
100
+ raise ValueError("Invitations must be a non-empty list")
101
+
102
+ results = []
103
+
104
+ for i, invitation in enumerate(invitations):
105
+ if not isinstance(invitation, dict):
106
+ raise ValueError(f"Invitation {i} must be a dictionary")
107
+
108
+ # Validate required fields
109
+ required_fields = ['email', 'first_name', 'last_name']
110
+ for field in required_fields:
111
+ if field not in invitation:
112
+ raise ValueError(f"Invitation {i} missing required field: {field}")
113
+
114
+ try:
115
+ # Use provided values or defaults
116
+ role = invitation.get('role', default_role)
117
+ message = invitation.get('message', default_message)
118
+
119
+ user = self.invite_user_to_organization(
120
+ org_id=org_id,
121
+ email=invitation['email'],
122
+ first_name=invitation['first_name'],
123
+ last_name=invitation['last_name'],
124
+ role=role,
125
+ message=message
126
+ )
127
+ results.append(user)
128
+
129
+ except Exception as e:
130
+ self.sdk.logger.error(f"Failed to invite user {invitation.get('email')}: {e}")
131
+ results.append(None)
132
+ # Continue with other invitations even if one fails
133
+
134
+ return results
135
+
136
+ def resend_invitation(self, org_id: str, email: str, message: Optional[str] = None) -> bool:
137
+ """
138
+ Resend invitation to a user who was previously invited but hasn't joined.
139
+
140
+ Note: This is a convenience method that attempts to re-invite the user.
141
+ The API may handle this as a new invitation if the previous one expired.
142
+
143
+ Args:
144
+ org_id: Organization ID
145
+ email: Email address of the user to re-invite
146
+ message: Custom invitation message (optional)
147
+
148
+ Returns:
149
+ True if resend was successful
150
+
151
+ Raises:
152
+ TallyfyError: If the request fails
153
+ ValueError: If parameters are invalid
154
+ """
155
+ self._validate_org_id(org_id)
156
+ self._validate_email(email)
157
+
158
+ # Since there's no specific resend endpoint, we'll need to get user info first
159
+ # and then send a new invitation. This is a limitation of the current API.
160
+
161
+ # For now, we'll use a generic approach - this would need to be updated
162
+ # based on the actual API capabilities for resending invitations
163
+ try:
164
+ # Create a basic invitation message for resending
165
+ if not message:
166
+ message = "This is a reminder to join Tallyfy - please accept your invitation to help us automate tasks between people."
167
+
168
+ # Note: Without knowing the user's name, we'll use placeholder values
169
+ # In a real implementation, you might want to store invitation data
170
+ # or retrieve user info from a pending invitations endpoint
171
+ result = self.invite_user_to_organization(
172
+ org_id=org_id,
173
+ email=email,
174
+ first_name="User", # Placeholder - would need actual name
175
+ last_name="Invite", # Placeholder - would need actual name
176
+ role="light",
177
+ message=message
178
+ )
179
+
180
+ return result is not None
181
+
182
+ except TallyfyError:
183
+ raise
184
+ except Exception as e:
185
+ self._handle_api_error(e, "resend invitation", org_id=org_id, email=email)
186
+
187
+ def invite_user_with_custom_role_permissions(self, org_id: str, email: str, first_name: str,
188
+ last_name: str, role: str,
189
+ custom_permissions: Optional[dict] = None,
190
+ message: Optional[str] = None) -> Optional[User]:
191
+ """
192
+ Invite a user with custom role and permissions (if supported by API).
193
+
194
+ Args:
195
+ org_id: Organization ID
196
+ email: Email address of the user to invite
197
+ first_name: First name of the user
198
+ last_name: Last name of the user
199
+ role: User role
200
+ custom_permissions: Custom permissions dictionary (if supported)
201
+ message: Custom invitation message
202
+
203
+ Returns:
204
+ User object for the invited user
205
+
206
+ Raises:
207
+ TallyfyError: If the request fails
208
+ ValueError: If parameters are invalid
209
+ """
210
+ # For now, this is the same as regular invitation since the API
211
+ # doesn't appear to support custom permissions in the invite endpoint
212
+ # This method exists for future extensibility
213
+
214
+ if custom_permissions:
215
+ self.sdk.logger.warning("Custom permissions are not currently supported in invitations")
216
+
217
+ return self.invite_user_to_organization(
218
+ org_id=org_id,
219
+ email=email,
220
+ first_name=first_name,
221
+ last_name=last_name,
222
+ role=role,
223
+ message=message
224
+ )
225
+
226
+ def get_invitation_template_message(self, org_name: Optional[str] = None,
227
+ custom_text: Optional[str] = None) -> str:
228
+ """
229
+ Generate a customized invitation message template.
230
+
231
+ Args:
232
+ org_name: Organization name to include in the message
233
+ custom_text: Additional custom text to include
234
+
235
+ Returns:
236
+ Formatted invitation message string
237
+ """
238
+ base_message = "Please join Tallyfy - it's going to help us automate tasks between people."
239
+
240
+ if org_name:
241
+ base_message = f"Please join {org_name} on Tallyfy - it's going to help us automate tasks between people."
242
+
243
+ if custom_text:
244
+ base_message += f"\n\n{custom_text}"
245
+
246
+ return base_message
247
+
248
+ def validate_invitation_data(self, invitation_data: dict) -> dict:
249
+ """
250
+ Validate and clean invitation data before sending.
251
+
252
+ Args:
253
+ invitation_data: Dictionary containing invitation fields
254
+
255
+ Returns:
256
+ Validated and cleaned invitation data
257
+
258
+ Raises:
259
+ ValueError: If validation fails
260
+ """
261
+ required_fields = ['email', 'first_name', 'last_name']
262
+
263
+ # Check required fields
264
+ for field in required_fields:
265
+ if field not in invitation_data or not invitation_data[field]:
266
+ raise ValueError(f"Missing required field: {field}")
267
+
268
+ # Validate specific fields
269
+ self._validate_email(invitation_data['email'])
270
+ self._validate_name(invitation_data['first_name'], "First name")
271
+ self._validate_name(invitation_data['last_name'], "Last name")
272
+
273
+ # Validate role if provided
274
+ if 'role' in invitation_data:
275
+ self._validate_role(invitation_data['role'])
276
+
277
+ # Clean and return data
278
+ cleaned_data = {
279
+ 'email': invitation_data['email'].strip().lower(),
280
+ 'first_name': invitation_data['first_name'].strip(),
281
+ 'last_name': invitation_data['last_name'].strip(),
282
+ 'role': invitation_data.get('role', 'light'),
283
+ 'message': invitation_data.get('message', self.get_invitation_template_message())
284
+ }
285
+
286
+ return cleaned_data