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.
- tallyfy/__init__.py +8 -4
- tallyfy/core.py +8 -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/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.5.dist-info}/METADATA +120 -56
- tallyfy-1.0.5.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.5.dist-info}/WHEEL +0 -0
- {tallyfy-1.0.4.dist-info → tallyfy-1.0.5.dist-info}/licenses/LICENSE +0 -0
- {tallyfy-1.0.4.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
|