sunholo 0.140.13__py3-none-any.whl → 0.142.0__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.
@@ -280,7 +280,7 @@ class DiscoveryEngineClient:
280
280
  # Use async_search_with_filters with filter_str=None to perform a regular search
281
281
  return await self.async_search_with_filters(
282
282
  query=query,
283
- filter_str=None,
283
+ filter_str=filter_str,
284
284
  num_previous_chunks=num_previous_chunks,
285
285
  num_next_chunks=num_next_chunks,
286
286
  page_size=page_size,
@@ -8,12 +8,39 @@ from tenacity import AsyncRetrying, retry_if_exception_type, wait_random_exponen
8
8
  log = setup_logging("sunholo_AsyncTaskRunner")
9
9
 
10
10
  class AsyncTaskRunner:
11
- def __init__(self, retry_enabled:bool=False, retry_kwargs:dict=None, timeout:int=120, max_concurrency:int=20):
11
+ def __init__(self,
12
+ retry_enabled: bool = False,
13
+ retry_kwargs: dict = None,
14
+ timeout: int = 120,
15
+ max_concurrency: int = 20,
16
+ heartbeat_extends_timeout: bool = False,
17
+ hard_timeout: int = None):
18
+ """
19
+ Initialize AsyncTaskRunner with configurable timeout behavior.
20
+
21
+ Args:
22
+ retry_enabled: Whether to enable retries
23
+ retry_kwargs: Retry configuration
24
+ timeout: Base timeout for tasks (seconds)
25
+ max_concurrency: Maximum concurrent tasks
26
+ heartbeat_extends_timeout: If True, heartbeats reset the timeout timer
27
+ hard_timeout: Maximum absolute timeout regardless of heartbeats (seconds).
28
+ If None, defaults to timeout * 5 when heartbeat_extends_timeout=True
29
+ """
12
30
  self.tasks = []
13
31
  self.retry_enabled = retry_enabled
14
32
  self.retry_kwargs = retry_kwargs or {}
15
33
  self.timeout = timeout
16
34
  self.semaphore = asyncio.Semaphore(max_concurrency)
35
+ self.heartbeat_extends_timeout = heartbeat_extends_timeout
36
+
37
+ # Set hard timeout
38
+ if hard_timeout is not None:
39
+ self.hard_timeout = hard_timeout
40
+ elif heartbeat_extends_timeout:
41
+ self.hard_timeout = timeout * 5 # Default to 5x base timeout
42
+ else:
43
+ self.hard_timeout = timeout # Same as regular timeout
17
44
 
18
45
  def add_task(self, func: Callable[..., Any], *args: Any, **kwargs: Any):
19
46
  """
@@ -39,9 +66,11 @@ class AsyncTaskRunner:
39
66
  for name, func, args, kwargs in self.tasks:
40
67
  log.info(f"Executing task: {name=}, {func=} with args: {args}, kwargs: {kwargs}")
41
68
  completion_event = asyncio.Event()
42
- task_coro = self._run_with_retries_and_timeout(name, func, args, kwargs, queue, completion_event)
69
+ last_heartbeat = {'time': time.time()} # Shared mutable object for heartbeat tracking
70
+
71
+ task_coro = self._run_with_retries_and_timeout(name, func, args, kwargs, queue, completion_event, last_heartbeat)
43
72
  task = asyncio.create_task(task_coro)
44
- heartbeat_coro = self._send_heartbeat(name, completion_event, queue)
73
+ heartbeat_coro = self._send_heartbeat(name, completion_event, queue, last_heartbeat)
45
74
  heartbeat_task = asyncio.create_task(heartbeat_coro)
46
75
  task_infos.append({
47
76
  'name': name,
@@ -93,9 +122,12 @@ class AsyncTaskRunner:
93
122
  args: tuple,
94
123
  kwargs: dict,
95
124
  queue: asyncio.Queue,
96
- completion_event: asyncio.Event) -> None:
125
+ completion_event: asyncio.Event,
126
+ last_heartbeat: dict) -> None:
97
127
  try:
98
128
  log.info(f"run_with_retries_and_timeout: {name=}, {func=} with args: {args}, kwargs: {kwargs}")
129
+ log.info(f"Timeout mode: heartbeat_extends_timeout={self.heartbeat_extends_timeout}, timeout={self.timeout}s, hard_timeout={self.hard_timeout}s")
130
+
99
131
  if self.retry_enabled:
100
132
  retry_kwargs = {
101
133
  'wait': wait_random_exponential(multiplier=1, max=60),
@@ -106,13 +138,13 @@ class AsyncTaskRunner:
106
138
  async for attempt in AsyncRetrying(**retry_kwargs):
107
139
  with attempt:
108
140
  log.info(f"Starting task '{name}' with retry")
109
- result = await asyncio.wait_for(self._execute_task(func, *args, **kwargs), timeout=self.timeout)
141
+ result = await self._execute_task_with_timeout(func, name, last_heartbeat, *args, **kwargs)
110
142
  await queue.put({'type': 'task_complete', 'func_name': name, 'result': result})
111
143
  log.info(f"Sent 'task_complete' message for task '{name}'")
112
144
  return
113
145
  else:
114
146
  log.info(f"Starting task '{name}' with no retry")
115
- result = await asyncio.wait_for(self._execute_task(func, *args, **kwargs), timeout=self.timeout)
147
+ result = await self._execute_task_with_timeout(func, name, last_heartbeat, *args, **kwargs)
116
148
  await queue.put({'type': 'task_complete', 'func_name': name, 'result': result})
117
149
  log.info(f"Sent 'task_complete' message for task '{name}'")
118
150
  except asyncio.TimeoutError:
@@ -125,6 +157,55 @@ class AsyncTaskRunner:
125
157
  log.info(f"Task '{name}' completed.")
126
158
  completion_event.set()
127
159
 
160
+ async def _execute_task_with_timeout(self, func: Callable[..., Any], name: str, last_heartbeat: dict, *args: Any, **kwargs: Any) -> Any:
161
+ """
162
+ Execute task with either fixed timeout or heartbeat-extendable timeout.
163
+ """
164
+ if not self.heartbeat_extends_timeout:
165
+ # Original behavior - fixed timeout
166
+ return await asyncio.wait_for(self._execute_task(func, *args, **kwargs), timeout=self.timeout)
167
+ else:
168
+ # New behavior - heartbeat extends timeout
169
+ return await self._execute_task_with_heartbeat_timeout(func, name, last_heartbeat, *args, **kwargs)
170
+
171
+ async def _execute_task_with_heartbeat_timeout(self, func: Callable[..., Any], name: str, last_heartbeat: dict, *args: Any, **kwargs: Any) -> Any:
172
+ """
173
+ Execute task with heartbeat-extendable timeout and hard timeout limit.
174
+ """
175
+ start_time = time.time()
176
+ task = asyncio.create_task(self._execute_task(func, *args, **kwargs))
177
+
178
+ while not task.done():
179
+ current_time = time.time()
180
+
181
+ # Check hard timeout first (absolute limit)
182
+ if current_time - start_time > self.hard_timeout:
183
+ task.cancel()
184
+ try:
185
+ await task
186
+ except asyncio.CancelledError:
187
+ pass
188
+ raise asyncio.TimeoutError(f"Hard timeout exceeded ({self.hard_timeout}s)")
189
+
190
+ # Check soft timeout (extends with heartbeats)
191
+ time_since_heartbeat = current_time - last_heartbeat['time']
192
+ if time_since_heartbeat > self.timeout:
193
+ task.cancel()
194
+ try:
195
+ await task
196
+ except asyncio.CancelledError:
197
+ pass
198
+ raise asyncio.TimeoutError(f"Timeout exceeded - no heartbeat for {self.timeout}s")
199
+
200
+ # Wait a bit before checking again
201
+ try:
202
+ await asyncio.wait_for(asyncio.shield(task), timeout=1.0)
203
+ break # Task completed
204
+ except asyncio.TimeoutError:
205
+ continue # Check timeouts again
206
+
207
+ return await task
208
+
128
209
  async def _execute_task(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
129
210
  """
130
211
  Executes the given task function and returns its result.
@@ -143,14 +224,16 @@ class AsyncTaskRunner:
143
224
  else:
144
225
  return await asyncio.to_thread(func, *args, **kwargs)
145
226
 
146
- async def _send_heartbeat(self, func_name: str, completion_event: asyncio.Event, queue: asyncio.Queue, interval: int = 2):
227
+ async def _send_heartbeat(self, func_name: str, completion_event: asyncio.Event, queue: asyncio.Queue, last_heartbeat: dict, interval: int = 2):
147
228
  """
148
229
  Sends periodic heartbeat updates to indicate the task is still in progress.
230
+ Updates last_heartbeat time if heartbeat_extends_timeout is enabled.
149
231
 
150
232
  Args:
151
233
  func_name (str): The name of the task function.
152
234
  completion_event (asyncio.Event): Event to signal when the task is completed.
153
235
  queue (asyncio.Queue): The queue to send heartbeat messages to.
236
+ last_heartbeat (dict): Mutable dict containing the last heartbeat time.
154
237
  interval (int): How frequently to send heartbeat messages (in seconds).
155
238
  """
156
239
  start_time = time.time()
@@ -158,7 +241,14 @@ class AsyncTaskRunner:
158
241
  try:
159
242
  while not completion_event.is_set():
160
243
  await asyncio.sleep(interval)
161
- elapsed_time = int(time.time() - start_time)
244
+ current_time = time.time()
245
+ elapsed_time = int(current_time - start_time)
246
+
247
+ # Update last heartbeat time if heartbeat extends timeout
248
+ if self.heartbeat_extends_timeout:
249
+ last_heartbeat['time'] = current_time
250
+ log.debug(f"Updated heartbeat time for task '{func_name}' at {current_time}")
251
+
162
252
  heartbeat_message = {
163
253
  'type': 'heartbeat',
164
254
  'name': func_name,
sunholo/utils/mime.py CHANGED
@@ -83,7 +83,6 @@ def get_mime_type_gemini(file_path:str) -> str:
83
83
 
84
84
  # Define the mapping of extensions to MIME types
85
85
  mime_types = {
86
-
87
86
  # Images
88
87
  'png': 'image/png',
89
88
  'jpg': 'image/jpeg',
@@ -111,7 +110,20 @@ def get_mime_type_gemini(file_path:str) -> str:
111
110
  'rtf': 'text/rtf',
112
111
 
113
112
  # Special case: JSON files are treated as plain text
114
- 'json': 'text/plain'
113
+ 'json': 'text/plain',
114
+
115
+ # Audio
116
+ 'mp3': 'audio/mp3',
117
+ 'mpeg': 'audio/mpeg',
118
+ 'wav': 'audio/wav',
119
+
120
+ # Video
121
+ 'mov': 'video/mov',
122
+ 'mp4': 'video/mp4',
123
+ 'mpg': 'video/mpeg',
124
+ 'avi': 'video/avi',
125
+ 'wmv': 'video/wmv',
126
+ 'flv': 'video/flv'
115
127
  }
116
128
 
117
129
  # Return the appropriate MIME type, defaulting to None if unknown
@@ -0,0 +1,25 @@
1
+
2
+
3
+ def convert_composite_to_native(value):
4
+ """
5
+ Recursively converts a proto MapComposite or RepeatedComposite object to native Python types.
6
+
7
+ Args:
8
+ value: The proto object, which could be a MapComposite, RepeatedComposite, or a primitive.
9
+
10
+ Returns:
11
+ The equivalent Python dictionary, list, or primitive type.
12
+ """
13
+ import proto
14
+
15
+ if isinstance(value, proto.marshal.collections.maps.MapComposite):
16
+ # Convert MapComposite to a dictionary, recursively processing its values
17
+ return {key: convert_composite_to_native(val) for key, val in value.items()}
18
+ elif isinstance(value, proto.marshal.collections.repeated.RepeatedComposite):
19
+ # Convert RepeatedComposite to a list, recursively processing its elements
20
+ return [convert_composite_to_native(item) for item in value]
21
+ else:
22
+ # If it's a primitive value, return it as is
23
+ return value
24
+
25
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sunholo
3
- Version: 0.140.13
3
+ Version: 0.142.0
4
4
  Summary: AI DevOps - a package to help deploy GenAI to the Cloud.
5
5
  Author-email: Holosun ApS <multivac@sunholo.com>
6
6
  License: Apache License, Version 2.0
@@ -255,7 +255,7 @@ vac:
255
255
  sunholo vac chat my-agent
256
256
  ```
257
257
 
258
- 4. **Deploy to Google Cloud Run:**
258
+ 4. **Run your agent as a local Flask app:**
259
259
  ```bash
260
260
  sunholo deploy my-agent
261
261
  ```
@@ -369,26 +369,32 @@ vac:
369
369
 
370
370
  ```bash
371
371
  # Project Management
372
- sunholo init <project-name> # Create new project
372
+ sunholo init <project-name> # Create new project from template
373
373
  sunholo list-configs # List all configurations
374
- sunholo list-configs --validate # Validate configs
374
+ sunholo list-configs --validate # Validate configurations
375
375
 
376
376
  # Development
377
377
  sunholo vac chat <vac-name> # Chat with a VAC locally
378
- sunholo vac list # List available VACs
378
+ sunholo vac list # List available VACs
379
+ sunholo vac get-url <vac-name> # Get Cloud Run URL for a VAC
379
380
  sunholo proxy start <service> # Start local proxy to cloud service
380
-
381
- # Deployment
382
- sunholo deploy <vac-name> # Deploy to Cloud Run
383
- sunholo deploy <vac-name> --dev # Deploy to dev environment
381
+ sunholo proxy list # List running proxies
382
+ sunholo deploy <vac-name> # Run Flask app locally
384
383
 
385
384
  # Document Processing
386
- sunholo embed <vac-name> # Embed documents
385
+ sunholo embed <vac-name> # Process and embed documents
387
386
  sunholo merge-text <folder> <output> # Merge files for context
388
387
 
389
388
  # Cloud Services
390
- sunholo discovery-engine create <name> # Create Discovery Engine
391
- sunholo proxy list # List running proxies
389
+ sunholo discovery-engine create <name> # Create Discovery Engine instance
390
+ sunholo vertex list-extensions # List Vertex AI extensions
391
+ sunholo swagger <vac-name> # Generate OpenAPI spec
392
+
393
+ # Integration Tools
394
+ sunholo excel-init # Initialize Excel plugin
395
+ sunholo llamaindex <query> # Query with LlamaIndex
396
+ sunholo mcp list-tools # List MCP tools
397
+ sunholo tts <text> # Text-to-speech synthesis
392
398
  ```
393
399
 
394
400
  ## 📝 Examples
@@ -74,7 +74,7 @@ sunholo/discovery_engine/__init__.py,sha256=hLgqRDJ22Aov9o2QjAEfsVgnL3kMdM-g5p8R
74
74
  sunholo/discovery_engine/chunker_handler.py,sha256=wkvXl4rFtYfN6AZUKdW9_QD49Whf77BukDbO82UwlAg,7480
75
75
  sunholo/discovery_engine/cli.py,sha256=tsKqNSDCEsDTz5-wuNwjttb3Xt35D97-KyyEiaqolMQ,35628
76
76
  sunholo/discovery_engine/create_new.py,sha256=WUi4_xh_dFaGX3xA9jkNKZhaR6LCELjMPeRb0hyj4FU,1226
77
- sunholo/discovery_engine/discovery_engine_client.py,sha256=VSInta9IZE_LmA3CFYqxLpxdoB7w0IuSRSFM2UnmrRk,63705
77
+ sunholo/discovery_engine/discovery_engine_client.py,sha256=0gMVWunSndh7uyT4IMSIkDKrNec3CLKX-om0eIjdC9o,63711
78
78
  sunholo/discovery_engine/get_ai_search_chunks.py,sha256=I6Dt1CznqEvE7XIZ2PkLqopmjpO96iVEWJJqL5cJjOU,5554
79
79
  sunholo/embedder/__init__.py,sha256=sI4N_CqgEVcrMDxXgxKp1FsfsB4FpjoXgPGkl4N_u4I,44
80
80
  sunholo/embedder/embed_chunk.py,sha256=did2pKkWM2o0KkRcb0H9l2x_WjCq6OyuHDxGbITFKPM,6530
@@ -96,7 +96,7 @@ sunholo/genai/init.py,sha256=yG8E67TduFCTQPELo83OJuWfjwTnGZsyACospahyEaY,687
96
96
  sunholo/genai/process_funcs_cls.py,sha256=D6eNrc3vtTZzwdkacZNOSfit499N_o0C5AHspyUJiYE,33690
97
97
  sunholo/genai/safety.py,sha256=mkFDO_BeEgiKjQd9o2I4UxB6XI7a9U-oOFjZ8LGRUC4,1238
98
98
  sunholo/invoke/__init__.py,sha256=o1RhwBGOtVK0MIdD55fAIMCkJsxTksi8GD5uoqVKI-8,184
99
- sunholo/invoke/async_class.py,sha256=OmRHaRf_yXNDWftWPpziytXh1TAhMumxnNhN_PGpQFs,8230
99
+ sunholo/invoke/async_class.py,sha256=ZMzxKQtelbYibu9Fac7P9OU3GorH8KxawZxSMv5EO9A,12514
100
100
  sunholo/invoke/direct_vac_func.py,sha256=dACx3Zh7uZnuWLIFYiyLoyXUhh5-eUpd2RatDUd9ov8,9753
101
101
  sunholo/invoke/invoke_vac_utils.py,sha256=sJc1edHTHMzMGXjji1N67c3iUaP7BmAL5nj82Qof63M,2053
102
102
  sunholo/langfuse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -155,8 +155,9 @@ sunholo/utils/config_class.py,sha256=U0xwyCz68KCJgzyhXd0AmbFnstMBFvZMedb-lLKKa5Q
155
155
  sunholo/utils/config_schema.py,sha256=Wv-ncitzljOhgbDaq9qnFqH5LCuxNv59dTGDWgd1qdk,4189
156
156
  sunholo/utils/gcp.py,sha256=lus1HH8YhFInw6QRKwfvKZq-Lz-2KQg4ips9v1I_3zE,4783
157
157
  sunholo/utils/gcp_project.py,sha256=Fa0IhCX12bZ1ctF_PKN8PNYd7hihEUfb90kilBfUDjg,1411
158
- sunholo/utils/mime.py,sha256=mELAiZcGa69PshBxV7y770E0K09YfX4Z4ZRBPL-7gXs,3352
158
+ sunholo/utils/mime.py,sha256=HqquYfnhekjzmzFXdP9DtO50ZljNV8swjcOlMDH8GTs,3656
159
159
  sunholo/utils/parsers.py,sha256=wES0fRn3GONoymRXOXt-z62HCoOiUvvFXa-MfKfjCls,6421
160
+ sunholo/utils/proto_convert.py,sha256=IMd4d7nat2MkJDueDiY2jKbQ7KSJ6tNd2P0ANxDq1mc,927
160
161
  sunholo/utils/timedelta.py,sha256=BbLabEx7_rbErj_YbNM0MBcaFN76DC4PTe4zD2ucezg,493
161
162
  sunholo/utils/user_ids.py,sha256=SQd5_H7FE7vcTZp9AQuQDWBXd4FEEd7TeVMQe1H4Ny8,292
162
163
  sunholo/utils/version.py,sha256=P1QAJQdZfT2cMqdTSmXmcxrD2PssMPEGM-WI6083Fck,237
@@ -168,9 +169,9 @@ sunholo/vertex/init.py,sha256=1OQwcPBKZYBTDPdyU7IM4X4OmiXLdsNV30C-fee2scQ,2875
168
169
  sunholo/vertex/memory_tools.py,sha256=tBZxqVZ4InTmdBvLlOYwoSEWu4-kGquc-gxDwZCC4FA,7667
169
170
  sunholo/vertex/safety.py,sha256=S9PgQT1O_BQAkcqauWncRJaydiP8Q_Jzmu9gxYfy1VA,2482
170
171
  sunholo/vertex/type_dict_to_json.py,sha256=uTzL4o9tJRao4u-gJOFcACgWGkBOtqACmb6ihvCErL8,4694
171
- sunholo-0.140.13.dist-info/licenses/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
172
- sunholo-0.140.13.dist-info/METADATA,sha256=4GvUi1znwq6b_Ohjtx4uMQMQH6o-DWF4mNg1so-sQhM,17843
173
- sunholo-0.140.13.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
174
- sunholo-0.140.13.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
175
- sunholo-0.140.13.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
176
- sunholo-0.140.13.dist-info/RECORD,,
172
+ sunholo-0.142.0.dist-info/licenses/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
173
+ sunholo-0.142.0.dist-info/METADATA,sha256=9v1AM0UCFJv_qx8vYOFO8gprTDulHCAxnMck7Jcmszs,18292
174
+ sunholo-0.142.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
175
+ sunholo-0.142.0.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
176
+ sunholo-0.142.0.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
177
+ sunholo-0.142.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5