tallyfy 1.0.4__py3-none-any.whl → 1.0.6__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 +11 -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/models.py +3 -1
  9. tallyfy/task_management/__init__.py +81 -0
  10. tallyfy/task_management/base.py +125 -0
  11. tallyfy/task_management/creation.py +221 -0
  12. tallyfy/task_management/retrieval.py +211 -0
  13. tallyfy/task_management/search.py +196 -0
  14. tallyfy/template_management/__init__.py +85 -0
  15. tallyfy/template_management/analysis.py +1093 -0
  16. tallyfy/template_management/automation.py +469 -0
  17. tallyfy/template_management/base.py +56 -0
  18. tallyfy/template_management/basic_operations.py +477 -0
  19. tallyfy/template_management/health_assessment.py +763 -0
  20. tallyfy/user_management/__init__.py +69 -0
  21. tallyfy/user_management/base.py +146 -0
  22. tallyfy/user_management/invitation.py +286 -0
  23. tallyfy/user_management/retrieval.py +339 -0
  24. {tallyfy-1.0.4.dist-info → tallyfy-1.0.6.dist-info}/METADATA +120 -56
  25. tallyfy-1.0.6.dist-info/RECORD +28 -0
  26. tallyfy/BUILD.md +0 -5
  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.4.dist-info/RECORD +0 -13
  32. {tallyfy-1.0.4.dist-info → tallyfy-1.0.6.dist-info}/WHEEL +0 -0
  33. {tallyfy-1.0.4.dist-info → tallyfy-1.0.6.dist-info}/licenses/LICENSE +0 -0
  34. {tallyfy-1.0.4.dist-info → tallyfy-1.0.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,411 @@
1
+ """
2
+ AI-powered form field suggestions
3
+ """
4
+
5
+ from typing import List, Dict, Any, Optional
6
+ from .base import FormFieldManagerBase
7
+ from ..models import Step, TallyfyError
8
+
9
+
10
+ class FormFieldSuggestions(FormFieldManagerBase):
11
+ """Handles AI-powered form field suggestions based on step content"""
12
+
13
+ def suggest_form_fields_for_step(self, org_id: str, template_id: str, step_id: str) -> List[Dict[str, Any]]:
14
+ """
15
+ AI-powered suggestions for relevant form fields based on step content.
16
+
17
+ Args:
18
+ org_id: Organization ID
19
+ template_id: Template ID
20
+ step_id: Step ID to analyze
21
+
22
+ Returns:
23
+ List of suggested form field configurations
24
+
25
+ Raises:
26
+ TallyfyError: If the request fails
27
+ """
28
+ self._validate_org_id(org_id)
29
+ self._validate_template_id(template_id)
30
+ self._validate_step_id(step_id)
31
+
32
+ try:
33
+ # Get the step details to analyze
34
+ step = self._get_step_details(org_id, template_id, step_id)
35
+ if not step:
36
+ raise TallyfyError(f"Step {step_id} not found")
37
+
38
+ # Get template context for better suggestions
39
+ template = self.sdk.get_template(org_id, template_id)
40
+ if not template:
41
+ raise TallyfyError(f"Template {template_id} not found")
42
+
43
+ # Analyze step content for intelligent suggestions
44
+ step_title = step.title.lower() if step.title else ''
45
+ step_summary = step.summary.lower() if step.summary else ''
46
+ step_type = step.step_type or 'task'
47
+ existing_fields = step.captures or []
48
+
49
+ # Combined text for analysis
50
+ step_text = f"{step_title} {step_summary}".strip()
51
+
52
+ suggestions = []
53
+
54
+ # Get field patterns for analysis
55
+ field_patterns = self._get_field_patterns()
56
+
57
+ # Check existing field types to avoid duplicates
58
+ existing_field_types = set()
59
+ existing_field_labels = set()
60
+ for field in existing_fields:
61
+ if hasattr(field, 'field_type'):
62
+ existing_field_types.add(field.field_type)
63
+ if hasattr(field, 'label'):
64
+ existing_field_labels.add(field.label.lower())
65
+
66
+ # Analyze step content against patterns
67
+ matched_patterns = []
68
+ for pattern_name, pattern_data in field_patterns.items():
69
+ keyword_matches = sum(1 for keyword in pattern_data['keywords'] if keyword in step_text)
70
+ if keyword_matches > 0:
71
+ matched_patterns.append((pattern_name, keyword_matches, pattern_data))
72
+
73
+ # Sort by relevance (number of keyword matches)
74
+ matched_patterns.sort(key=lambda x: x[1], reverse=True)
75
+
76
+ # Generate suggestions from matched patterns
77
+ suggested_count = 0
78
+ max_suggestions = 5
79
+
80
+ for pattern_name, matches, pattern_data in matched_patterns:
81
+ if suggested_count >= max_suggestions:
82
+ break
83
+
84
+ for field_config in pattern_data['fields']:
85
+ if suggested_count >= max_suggestions:
86
+ break
87
+
88
+ # Skip if similar field already exists
89
+ field_label_lower = field_config['label'].lower()
90
+ if field_label_lower in existing_field_labels:
91
+ continue
92
+
93
+ # Add suggestion with metadata
94
+ suggestion = {
95
+ 'field_config': field_config.copy(),
96
+ 'confidence': min(0.9, 0.3 + (matches * 0.2)), # Confidence based on keyword matches
97
+ 'pattern_matched': pattern_name,
98
+ 'keyword_matches': matches,
99
+ 'priority': 'high' if matches >= 2 else 'medium' if matches >= 1 else 'low'
100
+ }
101
+
102
+ # Add position suggestion
103
+ suggestion['field_config']['position'] = len(existing_fields) + suggested_count + 1
104
+
105
+ suggestions.append(suggestion)
106
+ suggested_count += 1
107
+
108
+ # If no specific patterns matched, provide generic useful fields
109
+ if not suggestions:
110
+ suggestions = self._get_generic_suggestions(existing_fields)
111
+
112
+ # Add implementation guidance
113
+ for suggestion in suggestions:
114
+ suggestion['implementation'] = {
115
+ 'method': 'add_form_field_to_step',
116
+ 'parameters': {
117
+ 'org_id': org_id,
118
+ 'template_id': template_id,
119
+ 'step_id': step_id,
120
+ 'field_data': suggestion['field_config']
121
+ }
122
+ }
123
+
124
+ return suggestions
125
+
126
+ except TallyfyError:
127
+ raise
128
+ except Exception as e:
129
+ self._handle_api_error(e, "suggest form fields for step", org_id=org_id, template_id=template_id, step_id=step_id)
130
+
131
+ def _get_step_details(self, org_id: str, template_id: str, step_id: str) -> Optional[Step]:
132
+ """
133
+ Get step details using CRUD operations to avoid circular imports.
134
+
135
+ Args:
136
+ org_id: Organization ID
137
+ template_id: Template ID
138
+ step_id: Step ID
139
+
140
+ Returns:
141
+ Step object or None if not found
142
+ """
143
+ from .crud_operations import FormFieldCRUD
144
+ crud = FormFieldCRUD(self.sdk)
145
+ return crud.get_step(org_id, template_id, step_id)
146
+
147
+ def _get_field_patterns(self) -> Dict[str, Dict[str, Any]]:
148
+ """
149
+ Get predefined field patterns for content analysis.
150
+
151
+ Returns:
152
+ Dictionary of field patterns with keywords and suggested fields
153
+ """
154
+ return {
155
+ # Approval and Review patterns
156
+ 'approval': {
157
+ 'keywords': ['approve', 'review', 'sign off', 'accept', 'reject', 'confirm'],
158
+ 'fields': [
159
+ {
160
+ 'field_type': 'dropdown',
161
+ 'label': 'Decision',
162
+ 'options': [
163
+ {'value': 'approved', 'label': 'Approved'},
164
+ {'value': 'rejected', 'label': 'Rejected'},
165
+ {'value': 'needs_revision', 'label': 'Needs Revision'}
166
+ ],
167
+ 'required': True,
168
+ 'reasoning': 'Approval steps typically need a decision field'
169
+ },
170
+ {
171
+ 'field_type': 'textarea',
172
+ 'label': 'Comments',
173
+ 'required': False,
174
+ 'reasoning': 'Comments are useful for providing feedback'
175
+ }
176
+ ]
177
+ },
178
+
179
+ # Contact and Communication patterns
180
+ 'contact': {
181
+ 'keywords': ['contact', 'call', 'email', 'phone', 'reach out', 'communicate'],
182
+ 'fields': [
183
+ {
184
+ 'field_type': 'text',
185
+ 'label': 'Contact Method',
186
+ 'required': True,
187
+ 'reasoning': 'Track how contact was made'
188
+ },
189
+ {
190
+ 'field_type': 'date',
191
+ 'label': 'Contact Date',
192
+ 'required': True,
193
+ 'reasoning': 'Record when contact was made'
194
+ },
195
+ {
196
+ 'field_type': 'textarea',
197
+ 'label': 'Contact Notes',
198
+ 'required': False,
199
+ 'reasoning': 'Document the conversation or interaction'
200
+ }
201
+ ]
202
+ },
203
+
204
+ # Document and File patterns
205
+ 'document': {
206
+ 'keywords': ['document', 'file', 'upload', 'attach', 'report', 'contract', 'agreement'],
207
+ 'fields': [
208
+ {
209
+ 'field_type': 'file',
210
+ 'label': 'Document Upload',
211
+ 'required': True,
212
+ 'reasoning': 'File upload for document-related steps'
213
+ },
214
+ {
215
+ 'field_type': 'text',
216
+ 'label': 'Document Title',
217
+ 'required': False,
218
+ 'reasoning': 'Name or title of the document'
219
+ },
220
+ {
221
+ 'field_type': 'textarea',
222
+ 'label': 'Document Description',
223
+ 'required': False,
224
+ 'reasoning': 'Brief description of the document'
225
+ }
226
+ ]
227
+ },
228
+
229
+ # Payment and Financial patterns
230
+ 'payment': {
231
+ 'keywords': ['payment', 'invoice', 'cost', 'price', 'amount', 'bill', 'expense'],
232
+ 'fields': [
233
+ {
234
+ 'field_type': 'number',
235
+ 'label': 'Amount',
236
+ 'required': True,
237
+ 'reasoning': 'Financial steps need amount tracking'
238
+ },
239
+ {
240
+ 'field_type': 'dropdown',
241
+ 'label': 'Currency',
242
+ 'options': [
243
+ {'value': 'USD', 'label': 'USD'},
244
+ {'value': 'EUR', 'label': 'EUR'},
245
+ {'value': 'GBP', 'label': 'GBP'}
246
+ ],
247
+ 'required': True,
248
+ 'reasoning': 'Specify currency for financial transactions'
249
+ },
250
+ {
251
+ 'field_type': 'date',
252
+ 'label': 'Payment Date',
253
+ 'required': False,
254
+ 'reasoning': 'Track when payment was made'
255
+ }
256
+ ]
257
+ },
258
+
259
+ # Quality and Testing patterns
260
+ 'quality': {
261
+ 'keywords': ['test', 'quality', 'check', 'verify', 'validate', 'inspect'],
262
+ 'fields': [
263
+ {
264
+ 'field_type': 'dropdown',
265
+ 'label': 'Test Result',
266
+ 'options': [
267
+ {'value': 'pass', 'label': 'Pass'},
268
+ {'value': 'fail', 'label': 'Fail'},
269
+ {'value': 'partial', 'label': 'Partial Pass'}
270
+ ],
271
+ 'required': True,
272
+ 'reasoning': 'Quality steps need result tracking'
273
+ },
274
+ {
275
+ 'field_type': 'textarea',
276
+ 'label': 'Test Notes',
277
+ 'required': False,
278
+ 'reasoning': 'Document test findings and issues'
279
+ },
280
+ {
281
+ 'field_type': 'number',
282
+ 'label': 'Score',
283
+ 'required': False,
284
+ 'reasoning': 'Numerical rating for quality assessment'
285
+ }
286
+ ]
287
+ },
288
+
289
+ # Schedule and Time patterns
290
+ 'schedule': {
291
+ 'keywords': ['schedule', 'meeting', 'appointment', 'deadline', 'due', 'time'],
292
+ 'fields': [
293
+ {
294
+ 'field_type': 'datetime',
295
+ 'label': 'Scheduled Time',
296
+ 'required': True,
297
+ 'reasoning': 'Scheduling steps need date and time'
298
+ },
299
+ {
300
+ 'field_type': 'text',
301
+ 'label': 'Location',
302
+ 'required': False,
303
+ 'reasoning': 'Meeting location or venue'
304
+ },
305
+ {
306
+ 'field_type': 'textarea',
307
+ 'label': 'Agenda',
308
+ 'required': False,
309
+ 'reasoning': 'Meeting agenda or notes'
310
+ }
311
+ ]
312
+ }
313
+ }
314
+
315
+ def _get_generic_suggestions(self, existing_fields: List) -> List[Dict[str, Any]]:
316
+ """
317
+ Get generic form field suggestions when no specific patterns match.
318
+
319
+ Args:
320
+ existing_fields: List of existing form fields on the step
321
+
322
+ Returns:
323
+ List of generic field suggestions
324
+ """
325
+ return [
326
+ {
327
+ 'field_config': {
328
+ 'field_type': 'textarea',
329
+ 'label': 'Notes',
330
+ 'required': False,
331
+ 'position': len(existing_fields) + 1
332
+ },
333
+ 'confidence': 0.6,
334
+ 'pattern_matched': 'generic',
335
+ 'keyword_matches': 0,
336
+ 'priority': 'medium',
337
+ 'reasoning': 'Notes field is useful for most steps to capture additional information'
338
+ },
339
+ {
340
+ 'field_config': {
341
+ 'field_type': 'dropdown',
342
+ 'label': 'Status',
343
+ 'options': [
344
+ {'value': 'completed', 'label': 'Completed'},
345
+ {'value': 'in_progress', 'label': 'In Progress'},
346
+ {'value': 'blocked', 'label': 'Blocked'}
347
+ ],
348
+ 'required': False,
349
+ 'position': len(existing_fields) + 2
350
+ },
351
+ 'confidence': 0.5,
352
+ 'pattern_matched': 'generic',
353
+ 'keyword_matches': 0,
354
+ 'priority': 'low',
355
+ 'reasoning': 'Status tracking can be helpful for workflow management'
356
+ }
357
+ ]
358
+
359
+ def suggest_field_improvements(self, org_id: str, template_id: str, step_id: str, field_id: str) -> List[Dict[str, Any]]:
360
+ """
361
+ Suggest improvements for an existing form field.
362
+
363
+ Args:
364
+ org_id: Organization ID
365
+ template_id: Template ID
366
+ step_id: Step ID
367
+ field_id: Form field ID to analyze
368
+
369
+ Returns:
370
+ List of improvement suggestions
371
+
372
+ Raises:
373
+ TallyfyError: If the request fails
374
+ """
375
+ try:
376
+ # Get current field options using options management
377
+ from .options_management import FormFieldOptions
378
+ options_manager = FormFieldOptions(self.sdk)
379
+
380
+ current_options = options_manager.get_dropdown_options(org_id, template_id, step_id, field_id)
381
+
382
+ improvements = []
383
+
384
+ # Analyze dropdown options if applicable
385
+ if current_options:
386
+ # Check for common improvements
387
+ if len(current_options) > 10:
388
+ improvements.append({
389
+ 'type': 'reduce_options',
390
+ 'priority': 'medium',
391
+ 'description': 'Consider reducing the number of dropdown options for better usability',
392
+ 'recommendation': 'Group similar options or use a search-enabled dropdown',
393
+ 'current_count': len(current_options)
394
+ })
395
+
396
+ # Check for inconsistent naming
397
+ option_lengths = [len(opt) for opt in current_options]
398
+ if max(option_lengths) - min(option_lengths) > 20:
399
+ improvements.append({
400
+ 'type': 'normalize_options',
401
+ 'priority': 'low',
402
+ 'description': 'Option labels have inconsistent lengths',
403
+ 'recommendation': 'Standardize option label lengths for better visual consistency'
404
+ })
405
+
406
+ return improvements
407
+
408
+ except TallyfyError:
409
+ raise
410
+ except Exception as e:
411
+ self._handle_api_error(e, "suggest field improvements", org_id=org_id, template_id=template_id, step_id=step_id, field_id=field_id)
tallyfy/models.py CHANGED
@@ -70,6 +70,7 @@ class User:
70
70
  date_format: Optional[str] = None
71
71
  last_known_ip: Optional[str] = None
72
72
  last_known_country: Optional[str] = None
73
+ role: Optional[str] = None
73
74
 
74
75
  @classmethod
75
76
  def from_dict(cls, data: Dict[str, Any]) -> 'User':
@@ -110,7 +111,8 @@ class User:
110
111
  status=data.get('status'),
111
112
  date_format=data.get('date_format'),
112
113
  last_known_ip=data.get('last_known_ip'),
113
- last_known_country=data.get('last_known_country')
114
+ last_known_country=data.get('last_known_country'),
115
+ role=data.get('role')
114
116
  )
115
117
 
116
118
 
@@ -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