tallyfy 1.0.3__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 (34) 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.3.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/README.md +0 -634
  27. tallyfy/form_fields_management.py +0 -582
  28. tallyfy/task_management.py +0 -356
  29. tallyfy/template_management.py +0 -2607
  30. tallyfy/user_management.py +0 -235
  31. tallyfy-1.0.3.dist-info/RECORD +0 -14
  32. {tallyfy-1.0.3.dist-info → tallyfy-1.0.5.dist-info}/WHEEL +0 -0
  33. {tallyfy-1.0.3.dist-info → tallyfy-1.0.5.dist-info}/licenses/LICENSE +0 -0
  34. {tallyfy-1.0.3.dist-info → tallyfy-1.0.5.dist-info}/top_level.txt +0 -0
@@ -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)
@@ -0,0 +1,211 @@
1
+ """
2
+ Task and process retrieval operations
3
+ """
4
+
5
+ from typing import List, Optional
6
+ from .base import TaskManagerBase
7
+ from ..models import Task, Run, TallyfyError
8
+
9
+
10
+ class TaskRetrieval(TaskManagerBase):
11
+ """Handles task and process retrieval operations"""
12
+
13
+ def get_my_tasks(self, org_id: str) -> List[Task]:
14
+ """
15
+ Get all tasks assigned to the current user in the organization.
16
+
17
+ Args:
18
+ org_id: Organization ID
19
+
20
+ Returns:
21
+ List of Task objects assigned to the current user
22
+
23
+ Raises:
24
+ TallyfyError: If the request fails
25
+ """
26
+ self._validate_org_id(org_id)
27
+
28
+ try:
29
+ endpoint = f"organizations/{org_id}/me/tasks"
30
+ response_data = self.sdk._make_request('GET', endpoint)
31
+
32
+ tasks_data = self._extract_data(response_data)
33
+ if tasks_data:
34
+ return [Task.from_dict(task_data) for task_data in tasks_data]
35
+ else:
36
+ self.sdk.logger.warning("Unexpected response format for tasks")
37
+ return []
38
+
39
+ except TallyfyError:
40
+ raise
41
+ except Exception as e:
42
+ self._handle_api_error(e, "get my tasks", org_id=org_id)
43
+
44
+ def get_user_tasks(self, org_id: str, user_id: int) -> List[Task]:
45
+ """
46
+ Get all tasks assigned to the given user in the organization.
47
+
48
+ Args:
49
+ org_id: Organization ID
50
+ user_id: User ID
51
+
52
+ Returns:
53
+ List of Task objects assigned to the given user ID
54
+
55
+ Raises:
56
+ TallyfyError: If the request fails
57
+ """
58
+ self._validate_org_id(org_id)
59
+ self._validate_user_id(user_id)
60
+
61
+ try:
62
+ endpoint = f"organizations/{org_id}/users/{user_id}/tasks"
63
+ params = {
64
+ 'per_page': '100',
65
+ 'sort_by': 'newest',
66
+ 'status': 'all',
67
+ 'with': 'run,threads_count,step,tags,folders,member_watchers.watcher'
68
+ }
69
+
70
+ response_data = self.sdk._make_request('GET', endpoint, params=params)
71
+
72
+ tasks_data = self._extract_data(response_data)
73
+ if tasks_data:
74
+ return [Task.from_dict(task_data) for task_data in tasks_data]
75
+ else:
76
+ self.sdk.logger.warning("Unexpected response format for user tasks")
77
+ return []
78
+
79
+ except TallyfyError:
80
+ raise
81
+ except Exception as e:
82
+ self._handle_api_error(e, "get user tasks", org_id=org_id, user_id=user_id)
83
+
84
+ def get_tasks_for_process(self, org_id: str, process_id: Optional[str] = None, process_name: Optional[str] = None) -> List[Task]:
85
+ """
86
+ Get all tasks for a given process (run).
87
+
88
+ Args:
89
+ org_id: Organization ID
90
+ process_id: Process (run) ID to get tasks for
91
+ process_name: Process (run) name to get tasks for (alternative to process_id)
92
+
93
+ Returns:
94
+ List of Task objects for the specified process
95
+
96
+ Raises:
97
+ TallyfyError: If the request fails
98
+ ValueError: If neither process_id nor process_name is provided
99
+ """
100
+ self._validate_org_id(org_id)
101
+
102
+ if not process_id and not process_name:
103
+ raise ValueError("Either process_id or process_name must be provided")
104
+
105
+ try:
106
+ # If process_name is provided but not process_id, search for the process first
107
+ if process_name and not process_id:
108
+ # We need to import TaskSearch here to avoid circular imports
109
+ from .search import TaskSearch
110
+ search = TaskSearch(self.sdk)
111
+ process_id = search.search_processes_by_name(org_id, process_name)
112
+
113
+ self._validate_process_id(process_id)
114
+
115
+ endpoint = f"organizations/{org_id}/runs/{process_id}/tasks"
116
+ response_data = self.sdk._make_request('GET', endpoint)
117
+
118
+ tasks_data = self._extract_data(response_data)
119
+ if tasks_data:
120
+ return [Task.from_dict(task_data) for task_data in tasks_data]
121
+ else:
122
+ self.sdk.logger.warning("Unexpected response format for process tasks")
123
+ return []
124
+
125
+ except TallyfyError:
126
+ raise
127
+ except Exception as e:
128
+ self._handle_api_error(e, "get tasks for process", org_id=org_id, process_id=process_id, process_name=process_name)
129
+
130
+ def get_organization_runs(self, org_id: str, with_data: Optional[str] = None,
131
+ form_fields_values: Optional[bool] = None,
132
+ owners: Optional[str] = None, task_status: Optional[str] = None,
133
+ groups: Optional[str] = None, status: Optional[str] = None,
134
+ folder: Optional[str] = None, checklist_id: Optional[str] = None,
135
+ starred: Optional[bool] = None, run_type: Optional[str] = None,
136
+ tag: Optional[str] = None) -> List[Run]:
137
+ """
138
+ Get all processes (runs) in the organization.
139
+
140
+ Args:
141
+ org_id: Organization ID
142
+ with_data: Comma-separated data to include (e.g., 'checklist,tasks,assets,tags')
143
+ form_fields_values: Include form field values
144
+ owners: Filter by specific member IDs
145
+ task_status: Filter by task status ('all', 'in-progress', 'completed')
146
+ groups: Filter by group IDs
147
+ status: Filter by process status ('active', 'problem', 'delayed', 'complete', 'archived')
148
+ folder: Filter by folder ID
149
+ checklist_id: Filter by template ID
150
+ starred: Filter by starred status
151
+ run_type: Filter by type ('procedure', 'form', 'document')
152
+ tag: Filter by tag ID
153
+
154
+ Returns:
155
+ List of Run objects
156
+
157
+ Raises:
158
+ TallyfyError: If the request fails
159
+ """
160
+ self._validate_org_id(org_id)
161
+
162
+ try:
163
+ endpoint = f"organizations/{org_id}/runs"
164
+
165
+ # Build parameters using base class helper
166
+ params = self._build_query_params(
167
+ with_=with_data, # Use with_ to avoid Python keyword conflict
168
+ form_fields_values=form_fields_values,
169
+ owners=owners,
170
+ task_status=task_status,
171
+ groups=groups,
172
+ status=status,
173
+ folder=folder,
174
+ checklist_id=checklist_id,
175
+ starred=starred,
176
+ type=run_type, # API expects 'type' parameter
177
+ tag=tag
178
+ )
179
+
180
+ # Handle the 'with' parameter specially due to Python keyword conflict
181
+ if with_data:
182
+ params['with'] = with_data
183
+ if 'with_' in params:
184
+ del params['with_']
185
+
186
+ response_data = self.sdk._make_request('GET', endpoint, params=params)
187
+
188
+ runs_data = self._extract_data(response_data)
189
+ if runs_data:
190
+ return [Run.from_dict(run_data) for run_data in runs_data]
191
+ else:
192
+ self.sdk.logger.warning("Unexpected response format for organization runs")
193
+ return []
194
+
195
+ except TallyfyError:
196
+ raise
197
+ except Exception as e:
198
+ self._handle_api_error(e, "get organization runs", org_id=org_id)
199
+
200
+ def get_organization_processes(self, org_id: str, **kwargs) -> List[Run]:
201
+ """
202
+ Alias for get_organization_runs for better naming consistency.
203
+
204
+ Args:
205
+ org_id: Organization ID
206
+ **kwargs: Same parameters as get_organization_runs
207
+
208
+ Returns:
209
+ List of Run objects
210
+ """
211
+ return self.get_organization_runs(org_id, **kwargs)
@@ -0,0 +1,196 @@
1
+ """
2
+ Task and process search operations
3
+ """
4
+
5
+ from typing import List
6
+ from .base import TaskManagerBase
7
+ from ..models import SearchResult, TallyfyError
8
+
9
+
10
+ class TaskSearch(TaskManagerBase):
11
+ """Handles task and process search operations"""
12
+
13
+ def search_processes_by_name(self, org_id: str, process_name: str) -> str:
14
+ """
15
+ Search for processes by name using the search endpoint.
16
+
17
+ Args:
18
+ org_id: Organization ID
19
+ process_name: Name or partial name of the process to search for
20
+
21
+ Returns:
22
+ Process ID of the found process
23
+
24
+ Raises:
25
+ TallyfyError: If no process found, multiple matches, or search fails
26
+ """
27
+ self._validate_org_id(org_id)
28
+
29
+ if not process_name or not isinstance(process_name, str):
30
+ raise ValueError("Process name must be a non-empty string")
31
+
32
+ try:
33
+ search_endpoint = f"organizations/{org_id}/search"
34
+ search_params = {
35
+ 'on': 'process',
36
+ 'per_page': '20',
37
+ 'search': process_name
38
+ }
39
+
40
+ search_response = self.sdk._make_request('GET', search_endpoint, params=search_params)
41
+
42
+ if isinstance(search_response, dict) and 'process' in search_response:
43
+ process_data = search_response['process']
44
+ if 'data' in process_data and process_data['data']:
45
+ processes = process_data['data']
46
+
47
+ # First try exact match (case-insensitive)
48
+ exact_matches = [p for p in processes if p['name'].lower() == process_name.lower()]
49
+ if exact_matches:
50
+ return exact_matches[0]['id']
51
+ elif len(processes) == 1:
52
+ # Single search result, use it
53
+ return processes[0]['id']
54
+ else:
55
+ # Multiple matches found, provide helpful error with options
56
+ match_names = [f"'{p['name']}'" for p in processes[:5]] # Show max 5
57
+ raise TallyfyError(f"Multiple processes found matching '{process_name}': {', '.join(match_names)}. Please be more specific.")
58
+ else:
59
+ raise TallyfyError(f"No process found matching name: {process_name}")
60
+ else:
61
+ raise TallyfyError(f"Search failed for process name: {process_name}")
62
+
63
+ except TallyfyError:
64
+ raise
65
+ except Exception as e:
66
+ self._handle_api_error(e, "search processes by name", org_id=org_id, process_name=process_name)
67
+
68
+ def search(self, org_id: str, search_query: str, search_type: str = "process", per_page: int = 20) -> List[SearchResult]:
69
+ """
70
+ Search for processes, templates, or tasks in the organization.
71
+
72
+ Args:
73
+ org_id: Organization ID
74
+ search_query: Text to search for
75
+ search_type: Type of search - 'process', 'blueprint', or 'task' (default: 'process'). blueprint equals template
76
+ per_page: Number of results per page (default: 20)
77
+
78
+ Returns:
79
+ List of SearchResult objects
80
+
81
+ Raises:
82
+ TallyfyError: If the request fails
83
+ ValueError: If search_type is not valid
84
+ """
85
+ self._validate_org_id(org_id)
86
+
87
+ if not search_query or not isinstance(search_query, str):
88
+ raise ValueError("Search query must be a non-empty string")
89
+
90
+ # Validate search type
91
+ valid_types = ["process", "blueprint", "task"]
92
+ if search_type not in valid_types:
93
+ raise ValueError(f"Search type must be one of: {', '.join(valid_types)}")
94
+
95
+ if per_page <= 0 or per_page > 100:
96
+ raise ValueError("per_page must be between 1 and 100")
97
+
98
+ try:
99
+ endpoint = f"organizations/{org_id}/search"
100
+ params = {
101
+ 'on': search_type,
102
+ 'per_page': str(per_page),
103
+ 'search': search_query
104
+ }
105
+
106
+ response_data = self.sdk._make_request('GET', endpoint, params=params)
107
+
108
+ search_data = self._extract_data(response_data, search_type)
109
+ if search_data:
110
+ return [SearchResult.from_dict(result_data, search_type) for result_data in search_data]
111
+ else:
112
+ self.sdk.logger.info(f"No {search_type} results found for query: {search_query}")
113
+ return []
114
+
115
+ except TallyfyError:
116
+ raise
117
+ except Exception as e:
118
+ self._handle_api_error(e, "search", org_id=org_id, search_query=search_query, search_type=search_type)
119
+
120
+ def search_processes(self, org_id: str, search_query: str, per_page: int = 20) -> List[SearchResult]:
121
+ """
122
+ Search for processes in the organization.
123
+
124
+ Args:
125
+ org_id: Organization ID
126
+ search_query: Text to search for
127
+ per_page: Number of results per page (default: 20)
128
+
129
+ Returns:
130
+ List of SearchResult objects for processes
131
+
132
+ Raises:
133
+ TallyfyError: If the request fails
134
+ """
135
+ return self.search(org_id, search_query, "process", per_page)
136
+
137
+ def search_templates(self, org_id: str, search_query: str, per_page: int = 20) -> List[SearchResult]:
138
+ """
139
+ Search for templates (blueprints) in the organization.
140
+
141
+ Args:
142
+ org_id: Organization ID
143
+ search_query: Text to search for
144
+ per_page: Number of results per page (default: 20)
145
+
146
+ Returns:
147
+ List of SearchResult objects for templates
148
+
149
+ Raises:
150
+ TallyfyError: If the request fails
151
+ """
152
+ return self.search(org_id, search_query, "blueprint", per_page)
153
+
154
+ def search_tasks(self, org_id: str, search_query: str, per_page: int = 20) -> List[SearchResult]:
155
+ """
156
+ Search for tasks in the organization.
157
+
158
+ Args:
159
+ org_id: Organization ID
160
+ search_query: Text to search for
161
+ per_page: Number of results per page (default: 20)
162
+
163
+ Returns:
164
+ List of SearchResult objects for tasks
165
+
166
+ Raises:
167
+ TallyfyError: If the request fails
168
+ """
169
+ return self.search(org_id, search_query, "task", per_page)
170
+
171
+ def find_process_by_name(self, org_id: str, process_name: str, exact_match: bool = True) -> List[SearchResult]:
172
+ """
173
+ Find processes by name with flexible matching options.
174
+
175
+ Args:
176
+ org_id: Organization ID
177
+ process_name: Name of the process to search for
178
+ exact_match: If True, only return exact matches (case-insensitive)
179
+
180
+ Returns:
181
+ List of SearchResult objects matching the criteria
182
+
183
+ Raises:
184
+ TallyfyError: If the request fails
185
+ """
186
+ results = self.search_processes(org_id, process_name)
187
+
188
+ if exact_match:
189
+ # Filter for exact matches (case-insensitive)
190
+ exact_results = []
191
+ for result in results:
192
+ if hasattr(result, 'name') and result.name.lower() == process_name.lower():
193
+ exact_results.append(result)
194
+ return exact_results
195
+
196
+ return results